├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .gitreview ├── .npmignore ├── AUTHORS.txt ├── CODE_OF_CONDUCT.md ├── History.md ├── LICENSE.txt ├── NOTICE.txt ├── README.md ├── docs ├── README.md ├── demo │ ├── Arrow-ltr-inline.png │ ├── Arrow-rtl-inline.png │ ├── Arrow-tb-inline.png │ ├── borderimage-ltr.png │ ├── borderimage-rtl.png │ ├── borderimage-tb-lr.png │ ├── borderimage-tb-rl.png │ └── index.html ├── favicon.svg ├── index.html ├── lib │ ├── codex-design-tokens │ │ └── theme-wikimedia-ui-1.0.0.css │ └── cssjanus.js └── site.css ├── package-lock.json ├── package.json ├── src └── cssjanus.js └── test ├── .eslintrc.json ├── bench.js ├── data.json └── unit.js /.eslintignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /docs/lib/ 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": "wikimedia/common", 4 | "env": { 5 | "node": true 6 | }, 7 | "ignorePatterns": [ 8 | "package*.json" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.nyc_output 2 | /coverage 3 | /dest 4 | /node_modules 5 | /test/fixture*.dat 6 | -------------------------------------------------------------------------------- /.gitreview: -------------------------------------------------------------------------------- 1 | [gerrit] 2 | host=gerrit.wikimedia.org 3 | port=29418 4 | project=mediawiki/libs/node-cssjanus.git 5 | track=1 6 | defaultrebase=0 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Development only 2 | # dotfiles and tests 3 | /.* 4 | /test/ 5 | -------------------------------------------------------------------------------- /AUTHORS.txt: -------------------------------------------------------------------------------- 1 | Principal authors and major contributors (alphabetically): 2 | 3 | Bryon Engelhardt 4 | Lindsey Simon 5 | Roan Kattouw 6 | Roozbeh Pournader 7 | Timo Tijhof 8 | Trevor Parscal 9 | YairRand 10 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | This project adheres to the [Wikimedia Code of Conduct](https://www.mediawiki.org/wiki/Special:MyLanguage/Code_of_Conduct). 2 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | # Release History 2 | 3 | ## v2.3.0 / 2024-08-06 4 | 5 | * Don't change `:dir()` pseudo-selector target (Ebrahim Byagowi). 6 | 7 | ## v2.2.0 / 2024-07-23 8 | 9 | * Flip `calc()` in four value notation (Moh'd Khier Abualruz). 10 | 11 | ## v2.1.0 / 2021-05-02 12 | 13 | * Fix unexpected flipping in selectors with the general sibling combinator (Mainframe98, [#85](https://github.com/cssjanus/cssjanus/issues/85)). 14 | 15 | ## v2.0.0 / 2020-08-23 16 | 17 | Node.js 10 or later is required. 18 | 19 | * Fix unexpected flipping in certain cases involving double quotes or comments in selectors (YairRand, [#35](https://github.com/cssjanus/cssjanus/issues/35)). 20 | * Drop support for Node.js 6 and 8 (Timo Tijhof). 21 | 22 | ## v1.3.2 / 2019-05-10 23 | 24 | * test: Add a large test case to catch backtrack limit problem (James Forrester). 25 | * Document a known backtrack issue, not yet known to affect Node.js (Timo Tijhof). 26 | 27 | ## v1.3.1 / 2018-10-15 28 | 29 | * Fix bug where `transform` didn't flip on lines without semicolon (YairRand). 30 | 31 | ## v1.3.0 / 2018-07-03 32 | 33 | * Fix unintended flipping of selectors containing a backslash (YairRand). 34 | * Make cssjanus.js compatible with Closure Compiler (Chris Scribner). 35 | * Drop support for Node.js 4; This release requires Node 6 (Timo Tijhof). 36 | 37 | ## v1.2.2 / 2018-02-11 38 | 39 | * build: Add 'files' publishing whitelist to package.json (Timo Tijhof). 40 | 41 | ## v1.2.1 / 2017-10-23 42 | 43 | * Drop support for Node.js v0.10 and v0.12 (Timo Tijhof). 44 | * test: Cover border-radius with three values (Timo Tijhof). 45 | 46 | ## v1.2.0 / 2017-03-14 47 | 48 | * Flip `translate(x[,y,z])` and `translateX(x)` (Ed Sanders). 49 | 50 | ## v1.1.3 / 2016-12-23 51 | 52 | * Do not flip offset-y in text-shadow, even when color isn't as first value (Ed Sanders). 53 | 54 | ## v1.1.2 / 2015-02-03 55 | 56 | * Support !important and slash in border-radius values (Dominik Schilling). 57 | 58 | ## v1.1.1 / 2014-11-19 59 | 60 | * Support !important in four-value declarations (Matthew Flaschen). 61 | 62 | ## v1.1.0 / 2014-09-23 63 | 64 | * Drop support for node.js v0.8 (Timo Tijhof). 65 | * Correct documentation of calculateNewBorderRadius (Ed Sanders). 66 | * test: Convert test cases to JSON (Timo Tijhof). 67 | * Do not flip unknown properties starting with "left" or "right" (Timo Tijhof). 68 | * Do not flip five or more consecutive values (Timo Tijhof). 69 | * Support CSS3 rgb(a) and hsl(a) color notation (Timo Tijhof). 70 | * Flip text-shadow and box-shadow (Timo Tijhof). 71 | * Account for attribute selectors in open brace lookahead (Timo Tijhof). 72 | * Account for minified values in border-radius (Roan Kattouw). 73 | * Flip border-style (Timo Tijhof). 74 | * Account for decimals and lack of vertical value in background-position (Roan Kattouw). 75 | 76 | ## v1.0.2 / 2014-01-28 77 | 78 | * Prevent issues with css selectors containing parentheses (Yoav Farhi). 79 | * Fix bgHorizontalPercentageRegExp to not be too greedy (Dion Hulse). 80 | * Support `/*!` syntax for @noflip (Tom Yam). 81 | 82 | ## v1.0.1 / 2013-08-08 83 | 84 | * Fix global variable leak (Trevor Parscal). 85 | 86 | ## v1.0.0 / 2012-06-28 87 | 88 | * Initial commit (Trevor Parscal). 89 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | CSSJanus. https://www.mediawiki.org/wiki/CSSJanus 2 | 3 | Copyright 2014 Trevor Parscal 4 | Copyright 2010 Roan Kattouw 5 | Copyright 2008 Google Inc. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm](https://img.shields.io/npm/v/cssjanus.svg?style=flat)](https://www.npmjs.com/package/cssjanus) 2 | [![Tested with QUnit](https://img.shields.io/badge/tested_with-qunit-9c3493.svg)](https://qunitjs.com/) 3 | 4 | # CSSJanus 5 | 6 | Convert CSS stylesheets between left-to-right and right-to-left. 7 | 8 | Based the original [Google project](https://code.google.com/p/cssjanus/). 9 | 10 | See **[Interactive demo](https://cssjanus.github.io)**. 11 | 12 | ## Install 13 | 14 | ```sh 15 | npm install cssjanus 16 | ``` 17 | 18 | ## Usage 19 | 20 | ```javascript 21 | var cssjanus = require( 'cssjanus' ); 22 | var rtlCss = cssjanus.transform( ltrCss ); 23 | ``` 24 | 25 | ``` 26 | transform( string css [, Object options ] ) : string 27 | ``` 28 | 29 | Parameters: 30 | 31 | * `css` Stylesheet to transform 32 | * `options`: Options object (optional) 33 | * `options.transformDirInUrl` (Boolean): Transform directions in URLs, such as `ltr` to `rtl`. Default: `false`. 34 | * `options.transformEdgeInUrl` (Boolean): Transform edges in URLs, such as `left` to `right`. Default: `false`. 35 | 36 | ### Preventing flipping 37 | 38 | If a rule is not meant to be flipped by CSSJanus, use a `/* @noflip */` comment to protect the rule. 39 | 40 | ```css 41 | .rule1 { 42 | /* Will be converted to margin-right */ 43 | margin-left: 1em; 44 | } 45 | /* @noflip */ 46 | .rule2 { 47 | /* Will be preserved as margin-left */ 48 | margin-left: 1em; 49 | } 50 | ``` 51 | 52 | ## CSS Logical 53 | 54 | We encourage and recommend use of 55 | [CSS logical properties](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_logical_properties_and_values) 56 | for the subset of CSS features where a native 57 | direction-aware version of a CSS property exists 58 | (be sure to check [browser support](https://caniuse.com/) 59 | for specific properties). You can, for example, set 60 | properties like `margin-inline-start` instead of 61 | `margin-left`, which the browser flips based on content 62 | direction, and work seamlessly alongside other CSS 63 | properties that CSSJanus flips instead. 64 | 65 | Note that CSS logical properties flip based on nearest 66 | content direction and content language, whereas CSSJanus 67 | is generally configured to flip by user language and 68 | UI direction. 69 | 70 | ## Integrations 71 | 72 | * **[css](https://www.npmjs.com/package/css)** parser: [rtl-converter](https://github.com/HosseinAlipour/rtl-converter). 73 | * **Grunt**: [grunt-cssjanus](https://gerrit.wikimedia.org/g/mediawiki/tools/grunt-cssjanus). 74 | * **PHP** port: [php-cssjanus](https://gerrit.wikimedia.org/g/mediawiki/libs/php-cssjanus/). 75 | * **Gulp**: [gulp-cssjanus](https://github.com/tomyam1/gulp-cssjanus). 76 | * **PostCSS**: [postcss-cssjanus](https://www.npmjs.com/package/postcss-janus). 77 | * **styled-components**: [styled-components-rtl](https://www.npmjs.com/package/styled-components-rtl). 78 | * **Stylis**: [stylis-plugin-rtl](https://www.npmjs.com/package/stylis-plugin-rtl). 79 | * **webpack**: [cssjanus-webpack](https://www.npmjs.com/package/@mooeypoo/cssjanus-webpack), [webpack-arabic-css](https://www.npmjs.com/package/webpackarabiccssplugin). 80 | 81 | ## Who uses CSSJanus? 82 | 83 | * **[Wikimedia Foundation](https://www.wikimedia.org/)**, the non-profit behind Wikipedia and other free knowledge projects. Used as part of [MediaWiki](https://www.mediawiki.org/wiki/MediaWiki) and [VisualEditor](https://gerrit.wikimedia.org/g/VisualEditor/VisualEditor) on [Wikipedia](https://ar.wikipedia.org/), and [more](https://doc.wikimedia.org/). 84 | * **[WordPress](https://wordpress.org/)**, a free and open-source content management system. Used for the interface of wp-admin and the default yearly themes. 85 | * **[styled-components](https://styled-components.com/)**, an ecosystem of visual primitives. Its RTL support is powered by CSSJanus. 86 | * **[AdminLTE](https://adminlte.io/)**, an open-source admin dashboard and control panel theme. See 87 | [AdminLTE-RTL](https://github.com/mmdsharifi/AdminLTE-RTL). 88 | 89 | ## See also 90 | 91 | * [Interactive demo](https://cssjanus.github.io) 92 | 93 | ## Contribute 94 | 95 | * Issue tracker: 96 | * Source code: 97 | * Submit patches via Gerrit: 98 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # CSSJanus Website 2 | 3 | ## Getting started 4 | 5 | It's a static site. To run a server, you can use the built-in 6 | capabilities of Python or PHP, like so: 7 | 8 | * `python3 -m http.server 4000` (Python 3) 9 | * `php -S localhost:4000` (PHP) 10 | 11 | Then view . 12 | -------------------------------------------------------------------------------- /docs/demo/Arrow-ltr-inline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikimedia/node-cssjanus/3019516671b2109a766b61d18671f74f2ecf1c09/docs/demo/Arrow-ltr-inline.png -------------------------------------------------------------------------------- /docs/demo/Arrow-rtl-inline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikimedia/node-cssjanus/3019516671b2109a766b61d18671f74f2ecf1c09/docs/demo/Arrow-rtl-inline.png -------------------------------------------------------------------------------- /docs/demo/Arrow-tb-inline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikimedia/node-cssjanus/3019516671b2109a766b61d18671f74f2ecf1c09/docs/demo/Arrow-tb-inline.png -------------------------------------------------------------------------------- /docs/demo/borderimage-ltr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikimedia/node-cssjanus/3019516671b2109a766b61d18671f74f2ecf1c09/docs/demo/borderimage-ltr.png -------------------------------------------------------------------------------- /docs/demo/borderimage-rtl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikimedia/node-cssjanus/3019516671b2109a766b61d18671f74f2ecf1c09/docs/demo/borderimage-rtl.png -------------------------------------------------------------------------------- /docs/demo/borderimage-tb-lr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikimedia/node-cssjanus/3019516671b2109a766b61d18671f74f2ecf1c09/docs/demo/borderimage-tb-lr.png -------------------------------------------------------------------------------- /docs/demo/borderimage-tb-rl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikimedia/node-cssjanus/3019516671b2109a766b61d18671f74f2ecf1c09/docs/demo/borderimage-tb-rl.png -------------------------------------------------------------------------------- /docs/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CSSJanus Live demos 5 | 6 | 7 | 8 | 343 | 344 | 345 |
346 | CSSJanus Live demos 347 | Convert CSS stylesheets between left-to-right and right-to-left. 348 |
349 | 350 |
351 |
352 |
353 |

354 | 355 | 356 |

357 |
358 |
359 |

Content direction

360 |
361 |
362 | Inline direction 363 |
364 |
365 |
366 |
367 |
368 | Block
direction 369 |
370 |
371 |
372 |
373 |
374 |

Floated content

375 |
Point to the end
376 |
Floatation
377 |

Cupcake ipsum dolor sit. Amet I love croissant chocolate chocolate cake gummi bears chocolate. Chocolate jelly beans I love jelly. Sweet roll pastry sweet. Tart lemon drops pie. Jelly candy canes brownie pie macaroon pudding pie.

378 |
379 |
380 |

Sides

381 |
Flotation
382 |

A floating box at the far end of the box has borders on two sides (using border-color with four values), and border-radius on the one visible corner. It also has a margin on two sides, with different units.

383 |
384 |
385 |

Vertical

386 |
387 |

This paragraph is in a container that has padding on each side, using the two-value ("padding: 0 20%;") syntax. When rotated (top to bottom), the values should be flipped, so that the padding is on the top and bottom.

388 |
389 |
390 |
391 |

Text

392 |

This paragraph is has a text-indent. Sed efficitur vulputate arcu a facilisis. Sed et libero volutpat, facilisis ex vel, dapibus nisi. Maecenas bibendum, est non consequat consectetur, tellus erat elementum tortor, ut aliquet tellus enim sed ligula. Sed luctus congue purus sit amet condimentum. Suspendisse est ligula, accumsan laoreet porta vel, rutrum finibus dui.

393 |
394 |
395 |

Spacing

396 |

padding and margin styles. Padding.

397 |

398 | This span has both padding and margin after the text, using margin-[direction]/padding-[direction] properties. When flipping from ltr to rtl, padding-right becomes padding-left. For tb-lr, it becomes padding-bottom. 399 |

400 |

401 | Lorem ipsum. This span has both padding and margin before the text, using the four-value syntax. (E.g. padding: 0 0 0 1em; ) 402 |

403 |

404 | Lorem ipsum. This span has both padding and margin on both sides of the text, using the three-value syntax. When rotated, this gets converted to four-value syntax. 405 |

406 |
407 |
408 |

Borders

409 |

A border and border radius.

410 |

This span has a red border after the text, with rounded corners.

411 |

This span has a thicker border closer to the preceding line, a thinner border to the inline-start direction, and repeating text at the border closer to the following line.

412 |
413 |
414 |

Shadows

415 |

Using box-shadow, text-shadow, including inset positioning and overlapping shadows.

416 |

This text has a red shadow before the text slightly closer to the previous line, and a blue shadow after the text and slightly closer to the following line.

417 |

This box has an inset shadow, visible before the text.

418 |
This box has a light orange shadow at the start and above the text. And a red shadow at the end and below the text.
419 |
420 |
421 |

Transform

422 |

transform: translate( 25%, 50% );

423 |
424 | Original 425 |
426 |
427 | Transformed
(25% towards end of line, 50% toward end of page) 428 |
429 |
430 |
431 |

Position

432 |

position: relative;

433 |
    434 |
  • Moved toward the following line.
  • 435 |
  • Moved toward the previous line.
  • 436 |
  • Moved forward in the direction of the text.
  • 437 |
  • Moved backward in the direction of the text.
  • 438 |
439 |
440 |
441 |

Cursors

442 |

Cursor is shown when hovering over the text.

443 |
    444 |
  • The cursor points in the text direction.
  • 445 |
  • The cursor points against the text direction.
  • 446 |
  • The cursor is perpendicular to the text direction.
  • 447 |
  • The cursor points to the diagonal between the block direction and inline direction.
  • 448 |
  • diagonal cursor
  • 449 |
  • row cursor
  • 450 |
  • column cursor
  • 451 |
452 |
453 |
454 |

Background image

455 |

Background-images, including URL flips, and positioning.

456 |
    457 |
  • Outer
  • 458 |
459 |
    460 |
  • This element has a background arrow pointing toward the text, positioned at the text beginning.
  • 461 |
  • This element has repeated background arrows on a line after the the text's block, pointing toward the inline direction.
  • 462 |
463 |
464 |
465 |
466 | 467 | 472 | 473 | 474 | 503 | 504 | -------------------------------------------------------------------------------- /docs/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | ↔️ 3 | 4 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CSSJanus 5 | 6 | 7 | 8 | 9 |
10 | CSSJanus 11 | Convert CSS stylesheets between left-to-right and right-to-left. 12 |
13 | 14 |
15 |
16 |
17 | 18 | 19 |
20 |
21 | 22 | 23 |
24 |
25 | 26 | 27 |
28 |
29 | 30 | 31 |
32 |
33 |
34 | 35 | 49 | 50 | 51 | 100 | 101 | -------------------------------------------------------------------------------- /docs/lib/codex-design-tokens/theme-wikimedia-ui-1.0.0.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Do not edit directly 3 | * Generated on Tue, 24 Oct 2023 19:12:13 GMT 4 | */ 5 | 6 | :root { 7 | --font-size-base: 16px; 8 | --font-size-x-small: 0.75em; /* `x` stands for extra. In this case extra small. */ 9 | --font-size-small: 0.875em; 10 | --font-size-medium: 1em; 11 | --font-size-large: 1.125em; 12 | --font-size-x-large: 1.25em; 13 | --font-size-xx-large: 1.5em; 14 | --font-size-xxx-large: 1.75em; 15 | --accent-color-base: #36c; 16 | --background-color-base: #fff; 17 | --background-color-interactive: #eaecf0; 18 | --background-color-interactive-subtle: #f8f9fa; 19 | --background-color-disabled: #c8ccd1; /* Components like Buttons, Checkboxes, Radios, ProgressBars…. */ 20 | --background-color-disabled-subtle: #eaecf0; /* Components like TextInputs, Selects…. */ 21 | --background-color-progressive: #36c; 22 | --background-color-progressive--hover: #447ff5; 23 | --background-color-progressive--active: #2a4b8d; 24 | --background-color-progressive--focus: #36c; 25 | --background-color-progressive-subtle: #eaf3ff; 26 | --background-color-destructive: #d73333; 27 | --background-color-destructive--hover: #ff4242; 28 | --background-color-destructive--active: #b32424; 29 | --background-color-destructive--focus: #d73333; 30 | --background-color-destructive-subtle: #fee7e6; 31 | --background-color-error: #d73333; 32 | --background-color-error--hover: #ff4242; 33 | --background-color-error--active: #b32424; 34 | --background-color-error-subtle: #fee7e6; 35 | --background-color-warning-subtle: #fef6e7; 36 | --background-color-success-subtle: #d5fdf4; 37 | --background-color-notice-subtle: #eaecf0; 38 | --background-color-backdrop-light: rgba( 255, 255, 255, 0.65 ); /* Backdrop is the term used by CSS Fullscreen API and is used to dim the background when a modal Dialog is open. Also known as overlay mask. */ 39 | --background-color-backdrop-dark: rgba( 0, 0, 0, 0.65 ); 40 | --background-color-transparent: rgba( 255, 255, 255, 0 ); 41 | --background-color-button-quiet--hover: rgba( 0, 24, 73, 0.027 ); 42 | --background-color-button-quiet--active: rgba( 0, 24, 73, 0.082 ); 43 | --background-color-input-binary--checked: #36c; 44 | --color-base: #202122; 45 | --color-base--hover: #404244; /* Aimed to be replaced by `color.gray600` in future. */ 46 | --color-emphasized: #000; 47 | --color-subtle: #54595d; 48 | --color-placeholder: #72777d; 49 | --color-disabled: #72777d; 50 | --color-inverted: #fff; 51 | --color-progressive: #36c; /* 'Progressive' Color and states */ 52 | --color-progressive--hover: #447ff5; 53 | --color-progressive--active: #2a4b8d; 54 | --color-progressive--focus: #36c; 55 | --color-destructive: #d73333; /* 'Destructive' Color and states */ 56 | --color-destructive--hover: #ff4242; 57 | --color-destructive--active: #b32424; 58 | --color-destructive--focus: #d73333; 59 | --color-visited: #6b4ba1; /* 'Visited' color. In combination with progressive. Used for default links. */ 60 | --color-error: #d73333; 61 | --color-warning: #edab00; /* Note, this is only meant for warning icon for contrast reasons. */ 62 | --color-success: #14866d; /* Note, this needs to use a tone darker than Red and Yellow for contrast reasons with background White. */ 63 | --color-notice: #202122; 64 | --color-link-red: #d73333; /* Red ('new') Link color and states */ 65 | --color-link-red--hover: #ff4242; 66 | --color-link-red--active: #b32424; 67 | --color-link-red--focus: #d73333; 68 | --color-link-red--visited: #a55858; 69 | --opacity-base: 1; 70 | --opacity-medium: 0.65; 71 | --opacity-low: 0.3; 72 | --opacity-transparent: 0; 73 | --opacity-icon-base: 0.87; 74 | --opacity-icon-base--hover: 0.74; 75 | --opacity-icon-base--selected: 1; 76 | --opacity-icon-base--disabled: 0.51; 77 | --opacity-icon-placeholder: 0.51; 78 | --opacity-icon-subtle: 0.67; 79 | --background-position-base: center; 80 | --background-size-search-figure: cover; /* Use in TypeaheadSearch and Thumbnail components for the thumb container. */ 81 | --z-index-bottom: -100; /* Use descriptive `z-index` tokens for layout purposes. */ 82 | --z-index-base: 0; 83 | --z-index-above-content: 1; 84 | --z-index-toolbar: 2; 85 | --z-index-dropdown: 50; 86 | --z-index-sticky: 100; 87 | --z-index-fixed: 200; 88 | --z-index-off-canvas-backdrop: 300; 89 | --z-index-off-canvas: 350; 90 | --z-index-overlay-backdrop: 400; 91 | --z-index-overlay: 450; 92 | --z-index-tooltip: 800; 93 | --z-index-toast-notification: 900; 94 | --z-index-top: 9999; 95 | --z-index-stacking-0: 0; /* Use stacking-specific z-index tokens inside components to layer individual elements. */ 96 | --z-index-stacking-1: 1; 97 | --z-index-stacking-2: 2; 98 | --z-index-stacking-3: 3; 99 | --box-sizing-base: border-box; 100 | --min-size-interactive-pointer: 32px; 101 | --min-size-interactive-touch: 44px; 102 | --min-size-search-figure: 40px; /* Alias for use in TypeaheadSearch and Thumbnail components for the same purpose and for better code readability. */ 103 | --min-size-icon-x-small: 12px; 104 | --min-size-icon-small: 16px; 105 | --min-size-icon-medium: 20px; 106 | --min-size-input-binary: 20px; 107 | --min-size-toggle-switch-grip: 20px; 108 | --size-0: 0; 109 | --size-6: 0.0625em; 110 | --size-12: 0.125em; 111 | --size-25: 0.25em; 112 | --size-50: 0.5em; 113 | --size-75: 0.75em; 114 | --size-100: 1em; 115 | --size-125: 1.25em; 116 | --size-150: 1.5em; 117 | --size-200: 2em; 118 | --size-250: 2.5em; 119 | --size-275: 2.75em; 120 | --size-300: 3em; 121 | --size-400: 4em; 122 | --size-800: 8em; 123 | --size-1600: 16em; 124 | --size-2400: 24em; 125 | --size-2800: 28em; 126 | --size-3200: 32em; 127 | --size-5600: 56em; 128 | --size-absolute-1: 1px; 129 | --size-absolute-9999: 9999px; 130 | --size-third: 33.33%; 131 | --size-half: 50%; 132 | --size-full: 100%; 133 | --size-double: 200%; 134 | --size-viewport-width-full: 100vw; 135 | --size-viewport-height-full: 100vh; 136 | --size-search-figure: 2.5em; /* Alias for use in TypeaheadSearch and Thumbnail components for the same purpose and for better code readability. */ 137 | --size-icon-x-small: 0.75em; 138 | --size-icon-small: 1em; 139 | --size-icon-medium: 1.25em; 140 | --min-width-medium: 256px; 141 | --min-width-breakpoint-mobile: 320px; /* A mobile device's minimum available screen width. Many older feature phones have screens smaller than this value. */ 142 | --min-width-breakpoint-tablet: 640px; /* A tablet device's minimum available screen width. Note: the size chosen is eager to be re-evaluated with Web team and Product Analytics. */ 143 | --min-width-breakpoint-desktop: 1120px; /* A desktop device's minimum available screen width. */ 144 | --min-width-breakpoint-desktop-wide: 1680px; /* A wider desktop's minimum available screen width. */ 145 | --min-width-toggle-switch: 48px; 146 | --max-width-base: none; 147 | --max-width-breakpoint-mobile: calc( 640px - 1px ); /* A mobile device's maximum available screen width. Many older feature phones have screens smaller than this value. */ 148 | --max-width-breakpoint-tablet: calc( 1120px - 1px ); /* A tablet device's maximum available screen width. Note, the size chosen is eager to be re-evaluated with Web team and Product Analytics. */ 149 | --max-width-breakpoint-desktop: calc( 1680px - 1px ); /* A desktop device's maximum available screen width. */ 150 | --max-width-button: 28em; /* Note, that this is a slight amendment from WikimediaUI Base from `28.75em` = `460px` to `448px` – `dimension.2800` */ 151 | --position-offset-border-width-base: -1px; 152 | --position-offset-input-radio--focus: -0.25em; 153 | --spacing-0: 0; 154 | --spacing-12: 2px; 155 | --spacing-25: 4px; 156 | --spacing-30: 5px; /* This token is an exception to the scale. Used as vertical `padding` in inputs and controls to achieve the default 32px component height. */ 157 | --spacing-35: 6px; /* This token is an exception to the scale. Used as `padding` of the ToggleSwitch component. */ 158 | --spacing-50: 8px; 159 | --spacing-75: 12px; 160 | --spacing-100: 16px; 161 | --spacing-125: 20px; 162 | --spacing-150: 24px; 163 | --spacing-200: 32px; 164 | --spacing-250: 40px; 165 | --spacing-300: 48px; 166 | --spacing-400: 64px; 167 | --spacing-half: 50%; /* From here on, spacing tokens which are used for positioning values. */ 168 | --spacing-full: 100%; 169 | --spacing-horizontal-button: 12px - 1px; /* Padding should equal 12px of spacing minus the width of the border */ 170 | --spacing-horizontal-button-icon-only: 6px - 1px; /* Padding should equal 6px of spacing minus the width of the border */ 171 | --spacing-horizontal-button-large: 16px - 1px; /* Padding should equal 16px of spacing minus the width of the border */ 172 | --spacing-horizontal-input-text-two-end-icons: calc( 8px * 2 + 1em ); /* Rely on `calc()` to make token output usable in all formats. When there are two end icons, (i.e. a clear icon and an end icon), we need to double the horizontal padding and account for the size of the extra icon. This token can be used to calculate the horizontal position of the clear icon and the padding-end of the text input. */ 173 | --spacing-start-typeahead-search-figure: 12px; /* The amount of space between the TypeaheadSearch figure's parent component and the TypeaheadSearch figure (input icon container, search result thumbnail, and footer icon container). We want this space to be uniform so that the figures vertically line up nicely. We use the same horizontal padding as the MenuItem. */ 174 | --spacing-start-typeahead-search-icon: calc( 12px + ( 40px - 20px ) / 2 ); /* The padding required for the icon to center align with the menu item thumbnail. We calculate the difference in size and add it to the expected spacing. */ 175 | --spacing-typeahead-search-focus-addition: calc( ( 12px + 40px ) - ( 20px + 8px ) ); /* The amount the width of the input increases when it is focused to allow for the extra spacing around the search figures. The caret position should remain static. This calculates the padding-left of the input when expanded minus the padding-left of the input when not expanded. (Note that both padding values actually include `@spacing-50` as well, but it was left out of the calculation for simplicity's sake.) */ 176 | --border-width-base: 1px; 177 | --border-width-thick: 2px; 178 | --border-width-input-radio--checked: 6px; 179 | --border-style-base: solid; 180 | --border-style-dashed: dashed; 181 | --border-color-base: #a2a9b1; 182 | --border-color-interactive: #72777d; 183 | --border-color-subtle: #c8ccd1; 184 | --border-color-disabled: #c8ccd1; 185 | --border-color-inverted: #fff; 186 | --border-color-progressive: #36c; 187 | --border-color-progressive--hover: #447ff5; 188 | --border-color-progressive--active: #2a4b8d; 189 | --border-color-progressive--focus: #36c; 190 | --border-color-destructive: #d73333; 191 | --border-color-destructive--hover: #ff4242; 192 | --border-color-destructive--active: #b32424; 193 | --border-color-destructive--focus: #d73333; 194 | --border-color-error: #b32424; 195 | --border-color-error--hover: #ff4242; 196 | --border-color-warning: #ac6600; 197 | --border-color-success: #096450; 198 | --border-color-notice: #54595d; 199 | --border-color-transparent: transparent; 200 | --border-color-divider: #a2a9b1; 201 | --border-color-input--hover: #72777d; 202 | --border-color-input-binary: #72777d; 203 | --border-color-input-binary--hover: #447ff5; 204 | --border-color-input-binary--active: #2a4b8d; 205 | --border-color-input-binary--focus: #36c; 206 | --border-color-input-binary--checked: #36c; 207 | --border-base: 1px solid #a2a9b1; 208 | --border-subtle: 1px solid #c8ccd1; 209 | --border-progressive: 1px solid #36c; 210 | --border-destructive: 1px solid #d73333; 211 | --border-radius-base: 2px; 212 | --border-radius-sharp: 0; 213 | --border-radius-pill: 9999px; 214 | --border-radius-circle: 50%; /* Use `50%` for circle or ellipsis. See https://stackoverflow.com/a/29966500 */ 215 | --box-shadow-drop-small: 0 1px 1px rgba( 0, 0, 0, 0.2 ); /* This features color as part of the theme token value. */ 216 | --box-shadow-drop-medium: 0 2px 2px 0 rgba( 0, 0, 0, 0.2 ); /* This features color as part of the theme token value. */ 217 | --box-shadow-drop-xx-large: 0 20px 48px 0 rgba( 0, 0, 0, 0.2 ); /* This features color as part of the theme token value. */ 218 | --box-shadow-inset-small: inset 0 0 0 1px; 219 | --box-shadow-inset-medium: inset 0 0 0 2px; 220 | --box-shadow-inset-medium-vertical: inset 0 -2px 0 0; 221 | --box-shadow-outset-small: 0 0 0 1px; 222 | --box-shadow-outset-small-top: 0 -1px 0 0; 223 | --box-shadow-outset-small-start: -1px 0 0 0; 224 | --box-shadow-color-base: #000; 225 | --box-shadow-color-progressive--active: #2a4b8d; 226 | --box-shadow-color-progressive--focus: #36c; 227 | --box-shadow-color-progressive-selected: #36c; 228 | --box-shadow-color-progressive-selected--hover: #447ff5; 229 | --box-shadow-color-progressive-selected--active: #2a4b8d; 230 | --box-shadow-color-destructive--focus: #d73333; 231 | --box-shadow-color-inverted: #fff; 232 | --box-shadow-color-transparent: transparent; 233 | --outline-base--focus: 1px solid transparent; /* Enable Windows high contrast mode on certain widgets, that have default outlines overridden. */ 234 | --outline-color-progressive--focus: #36c; /* Use in places where no more customized focus styles are provided, for example on generic `:focus`. */ 235 | --font-family-base: sans-serif; /* Reference Vector's default fallback sans instead of the deprecated value `font-family-sans` in WikimediaUI Base. */ 236 | --font-family-system-sans: -apple-system, 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Inter', 'Helvetica', 'Arial', sans-serif; 237 | --font-family-sans--fallback: sans-serif; 238 | --font-family-serif: 'Linux Libertine', 'Georgia', 'Times', 'Source Serif Pro', serif; 239 | --font-family-serif--fallback: serif; 240 | --font-family-monospace: 'Menlo', 'Consolas', 'Liberation Mono', 'Fira Code', 'Courier New', monospace; 241 | --font-family-monospace--fallback: monospace, monospace; 242 | --font-family-heading-main: 'Linux Libertine', 'Georgia', 'Times', 'Source Serif Pro', serif; /* Legacy value from WikimediaUI Base. Use for first heading special treatment. */ 243 | --font-weight-hairline: 100; 244 | --font-weight-light: 300; 245 | --font-weight-normal: 400; 246 | --font-weight-semi-bold: 600; 247 | --font-weight-bold: 700; 248 | --line-height-xxx-small: 1.25; /* `x` stands for extra. In this case extra-extra-extra small. */ 249 | --line-height-xx-small: 1.375; 250 | --line-height-small: 1.5714285; 251 | --line-height-medium: 1.6; 252 | --text-decoration-none: none; 253 | --text-decoration-line-through: line-through; 254 | --text-decoration-underline: underline; 255 | --text-overflow-clip: clip; 256 | --text-overflow-ellipsis: ellipsis; 257 | --tab-size-base: 4; 258 | --transition-duration-base: 100ms; 259 | --transition-duration-medium: 250ms; 260 | --transition-timing-function-system: ease; 261 | --transition-timing-function-user: ease-out; 262 | --transition-property-base: background-color, color, border-color, box-shadow; 263 | --transition-property-fade: opacity; 264 | --transition-property-icon: color; 265 | --transition-property-icon-css-only: background-color; 266 | --transition-property-toggle-switch-grip: background-color, border-color, transform; 267 | --animation-delay-none: 0ms; 268 | --animation-delay-medium: -160ms; 269 | --animation-delay-slow: -330ms; 270 | --animation-duration-medium: 1600ms; 271 | --animation-duration-slow: 2000ms; 272 | --animation-timing-function-base: linear; 273 | --animation-timing-function-bouncing: ease-in-out; 274 | --animation-iteration-count-base: infinite; 275 | --cursor-base: default; 276 | --cursor-base--disabled: default; 277 | --cursor-base--hover: pointer; 278 | --cursor-grab: grab; 279 | --cursor-grabbing: grabbing; 280 | --cursor-help: help; 281 | --cursor-move: move; 282 | --cursor-not-allowed: not-allowed; 283 | --cursor-resize-nesw: nesw-resize; 284 | --cursor-resize-nwse: nwse-resize; 285 | --cursor-text: text; 286 | --cursor-zoom-in: zoom-in; 287 | --cursor-zoom-out: zoom-out; 288 | --unit-absolute: px; 289 | --unit-relative: em; 290 | --background-image-input-checkbox--checked: url( 'data:image/svg+xml;utf8,' ); 291 | --min-height-text-area: 64px; 292 | 293 | /* DEPRECATED TOKENS */ 294 | /* Warning: the following token name is deprecated (No replacement for this token. Only used in non-Codex products and design decision is to need to rely on background for this state.) */ 295 | --background-color-primary--hover: rgba( 51, 102, 204, 0.1 ); 296 | /* Warning: the following token name is deprecated (Use `color-subtle` instead. Note, that `color-subtle` is using `color.gray600` instead of `color.gray500` now.) */ 297 | --color-base--subtle: #72777d; 298 | /* Warning: the following token name is deprecated, use min-width-breakpoint-mobile instead. (Legacy breakpoint. Use `min-width`/`max-width` breakpoint token instead. A mobile device's screen width. Many older feature phones have screens smaller than this value.) */ 299 | --width-breakpoint-mobile: 320px; 300 | /* Warning: the following token name is deprecated (Legacy breakpoint. Use `min-width`/`max-width` breakpoint token instead. A tablet device's screen width.) */ 301 | --width-breakpoint-tablet: 720px; 302 | /* Warning: the following token name is deprecated (Legacy breakpoint. Use `min-width`/`max-width` breakpoint token instead. A desktop device's screen width.) */ 303 | --width-breakpoint-desktop: 1000px; 304 | /* Warning: the following token name is deprecated (Legacy breakpoint. Use `min-width`/`max-width` breakpoint token instead. A wider desktop's screen width.) */ 305 | --width-breakpoint-desktop-wide: 1200px; 306 | /* Warning: the following token name is deprecated (Legacy breakpoint. Use `min-width`/`max-width` breakpoint token instead. An extra wide desktop's screen width.) */ 307 | --width-breakpoint-desktop-extrawide: 2000px; 308 | /* Warning: the following token name is deprecated (Use `font-family-sans--fallback` instead. See T247166.) */ 309 | --font-family-sans: 'Helvetica Neue', 'Helvetica', 'Liberation Sans', 'Arial', sans-serif; 310 | /* Warning: the following token name is deprecated (Legacy line-height. Use `line-height-small` instead.) */ 311 | --line-height-x-small: 1.4285714; 312 | } 313 | -------------------------------------------------------------------------------- /docs/lib/cssjanus.js: -------------------------------------------------------------------------------- 1 | ../../src/cssjanus.js -------------------------------------------------------------------------------- /docs/site.css: -------------------------------------------------------------------------------- 1 | /*! CSSJanus Demo | Based on */ 2 | 3 | :root { 4 | --cssjanus-desktop-wide: 1200px; 5 | --cssjanus-background-color-neutral: #eaecf0; 6 | --cssjanus-background-color-neutral-subtle: #f8f9fa; 7 | --cssjanus-border-color-soft: #eaecf0; 8 | } 9 | 10 | html { 11 | background-color: var( --cssjanus-background-color-neutral ); 12 | font-family: sans-serif; /* Basic support without css-variables */ 13 | font-family: var( --font-family-system-sans ); 14 | line-height: var( --line-height-medium ); 15 | font-size: 62.5%; 16 | color: var( --color-base ); 17 | } 18 | 19 | body { 20 | margin: 0; 21 | font-size: 1.6rem; 22 | } 23 | 24 | /* Header */ 25 | 26 | header .wm-container { 27 | display: flex; 28 | flex-flow: row wrap; 29 | justify-content: space-between; 30 | } 31 | 32 | header [role="banner"] { 33 | line-height: 4.9rem; 34 | font-weight: var( --font-weight-bold ); 35 | color: inherit; 36 | } 37 | header [role="banner"] em { 38 | font-weight: normal; 39 | font-style: normal; 40 | } 41 | 42 | .wm-header-caption { 43 | line-height: 4.9rem; 44 | } 45 | @media ( max-width: 768px ) { 46 | .wm-header-caption { 47 | display: none; 48 | } 49 | } 50 | 51 | /* Body */ 52 | 53 | .wm-container { 54 | margin: 0 auto; 55 | max-width: var( --cssjanus-desktop-wide ); 56 | padding: 0 1rem; 57 | box-sizing: border-box; 58 | } 59 | @media ( min-width: 720px ) { 60 | /* Beyond --width-breakpoint-tablet */ 61 | .wm-container { 62 | padding: 0 3.2rem; 63 | } 64 | } 65 | 66 | p { 67 | line-height: 1.5; 68 | } 69 | 70 | a { 71 | color: var( --color-progressive ); 72 | text-decoration: none; 73 | } 74 | a:hover { 75 | text-decoration: underline; 76 | text-underline-position: under; 77 | } 78 | 79 | code { 80 | display: inline-block; 81 | font-size: 1.4rem; 82 | background-color: var( --cssjanus-background-color-neutral-subtle ); 83 | border: var( --border-width-base ) var( --border-style-base ) var( --cssjanus-border-color-soft ); 84 | border-radius: var( --border-radius-base ); 85 | padding: 0.2rem 0.4rem; 86 | } 87 | 88 | main { 89 | background: var( --background-color-base ); 90 | box-shadow: 0 1px 4px 0 rgba( 0, 0, 0, 0.25 ); 91 | padding: 3.2rem 0 6rem 0; 92 | } 93 | 94 | ul { 95 | margin: 1rem; 96 | padding: 0; 97 | } 98 | ul ul { 99 | margin: 0 0 0 1em; 100 | } 101 | 102 | .wm-btn { 103 | display: inline-block; 104 | padding: 5px 12px; 105 | background-color: var( --background-color-interactive-subtle ); 106 | border-radius: var( --border-radius-base ); 107 | border: var( --border-base ); 108 | color: var( --color-base ); 109 | font-weight: var( --font-weight-bold ); 110 | font-size: 1.4rem; 111 | line-height: 1.42857143; 112 | cursor: pointer; 113 | } 114 | .wm-btn:hover { 115 | background-color: var( --background-color-base ); 116 | border-color: var( --border-color-base ); 117 | color: var( --color-base--hover ); 118 | } 119 | .wm-btn--progressive { 120 | background-color: var( --background-color-progressive ); 121 | border-color: var( --border-color-progressive ); 122 | color: var( --color-inverted ); 123 | } 124 | .wm-btn--progressive:hover { 125 | background-color: var( --background-color-progressive--hover ); 126 | border-color: var( --border-color-progressive--hover ); 127 | color: var( --color-inverted ); 128 | } 129 | .wm-btn--progressive:active { 130 | background-color: var( --color-progressive--active); 131 | border-color: var( --border-color-progressive--active ); 132 | color: var( --color-inverted ); 133 | } 134 | 135 | /* Footer */ 136 | 137 | footer { 138 | padding: 2.4rem 0; 139 | font-size: 1.3rem; 140 | } 141 | 142 | @media ( min-width: 768px ) { 143 | /* Render bullet list as horizontal line when there is more space */ 144 | footer nav ul { 145 | margin: 0; 146 | } 147 | footer nav ul li { 148 | list-style: none; 149 | display: inline-block; 150 | padding: 0 0.8rem 0 0; 151 | } 152 | footer nav li:not( :last-child )::after { 153 | content: "\2022"; 154 | color: var( --color-subtle ); 155 | padding: 0 0 0 0.8rem; 156 | } 157 | } 158 | 159 | /* CSSJanus */ 160 | 161 | .cssjanus-form { 162 | display: flex; 163 | flex-flow: row wrap; 164 | column-gap: 30px; 165 | row-gap: 10px; 166 | } 167 | 168 | .cssjanus-form label { 169 | display: block; 170 | margin-bottom: var( --spacing-25 ); 171 | } 172 | 173 | .cssjanus-form > * { 174 | flex-grow: 1; 175 | } 176 | 177 | .cssjanus-form-ioside { 178 | min-width: 300px; 179 | } 180 | 181 | .cssjanus-form-ioside label { 182 | font-weight: var( --font-weight-bold ); 183 | } 184 | 185 | .cssjanus-form-ioside textarea { 186 | width: 100%; 187 | font-family: var( --font-family-monospace ); 188 | font-size: 1.4rem; 189 | border: var( --border-base ); 190 | max-height: 80vh; 191 | padding: var( --spacing-30 ) var( --spacing-50 ); 192 | } 193 | 194 | textarea[readonly], 195 | fieldset[disabled] { 196 | cursor: not-allowed; 197 | background-color: var( --background-color-interactive-subtle ); 198 | opacity: 1; 199 | } 200 | 201 | .cssjanus-form-row { 202 | width: 100%; 203 | } 204 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cssjanus", 3 | "version": "2.3.0", 4 | "description": "Convert CSS stylesheets between left-to-right and right-to-left directions.", 5 | "author": "Trevor Parscal (https://trevorparscal.com/)", 6 | "license": "Apache-2.0", 7 | "homepage": "https://www.mediawiki.org/wiki/CSSJanus", 8 | "bugs": { 9 | "url": "https://phabricator.wikimedia.org/tag/cssjanus/" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://gerrit.wikimedia.org/g/mediawiki/libs/node-cssjanus" 14 | }, 15 | "keywords": [ 16 | "i18n", 17 | "bidi", 18 | "rtl", 19 | "ltr" 20 | ], 21 | "main": "./src/cssjanus.js", 22 | "files": [ 23 | "src/", 24 | "History.md", 25 | "LICENSE.txt" 26 | ], 27 | "scripts": { 28 | "doc": "rm docs/lib/cssjanus.js && cp src/cssjanus.js docs/lib/cssjanus.js", 29 | "renew-doc-deps": "curl -s https://unpkg.com/@wikimedia/codex-design-tokens@1.0.0/theme-wikimedia-ui.css -o docs/lib/codex-design-tokens/theme-wikimedia-ui-1.0.0.css", 30 | "test": "eslint . && qunit test/unit.js", 31 | "coverage": "nyc qunit test/unit.js" 32 | }, 33 | "engines": { 34 | "node": ">=10.0.0" 35 | }, 36 | "dependencies": {}, 37 | "devDependencies": { 38 | "eslint": "8.57.0", 39 | "eslint-config-wikimedia": "0.30.0", 40 | "nyc": "^15.1.0", 41 | "qunit": "2.24.1" 42 | }, 43 | "nyc": { 44 | "include": [ 45 | "src" 46 | ], 47 | "report-dir": "coverage", 48 | "reporter": [ 49 | "text", 50 | "html", 51 | "clover" 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/cssjanus.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * CSSJanus. https://www.mediawiki.org/wiki/CSSJanus 3 | * 4 | * Copyright 2014 Trevor Parscal 5 | * Copyright 2010 Roan Kattouw 6 | * Copyright 2008 Google Inc. 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | var cssjanus; 22 | 23 | /** 24 | * Create a tokenizer object. 25 | * 26 | * This utility class is used by CSSJanus to protect strings by replacing them temporarily with 27 | * tokens and later transforming them back. 28 | * 29 | * @class 30 | * @constructor 31 | * @param {RegExp} regex Regular expression whose matches to replace by a token 32 | * @param {string} token Placeholder text 33 | */ 34 | function Tokenizer( regex, token ) { 35 | 36 | var matches = [], 37 | index = 0; 38 | 39 | /** 40 | * Add a match. 41 | * 42 | * @private 43 | * @param {string} match Matched string 44 | * @return {string} Token to leave in the matched string's place 45 | */ 46 | function tokenizeCallback( match ) { 47 | matches.push( match ); 48 | return token; 49 | } 50 | 51 | /** 52 | * Get a match. 53 | * 54 | * @private 55 | * @return {string} Original matched string to restore 56 | */ 57 | function detokenizeCallback() { 58 | return matches[ index++ ]; 59 | } 60 | 61 | return { 62 | /** 63 | * Replace matching strings with tokens. 64 | * 65 | * @param {string} str String to tokenize 66 | * @return {string} Tokenized string 67 | */ 68 | tokenize: function ( str ) { 69 | return str.replace( regex, tokenizeCallback ); 70 | }, 71 | 72 | /** 73 | * Restores tokens to their original values. 74 | * 75 | * @param {string} str String previously run through tokenize() 76 | * @return {string} Original string 77 | */ 78 | detokenize: function ( str ) { 79 | return str.replace( new RegExp( '(' + token + ')', 'g' ), detokenizeCallback ); 80 | } 81 | }; 82 | } 83 | 84 | /** 85 | * Create a CSSJanus object. 86 | * 87 | * CSSJanus transforms CSS rules with horizontal relevance so that a left-to-right stylesheet can 88 | * become a right-to-left stylesheet automatically. Processing can be bypassed for an entire rule 89 | * or a single property by adding a / * @noflip * / comment above the rule or property. 90 | * 91 | * @class 92 | * @constructor 93 | */ 94 | function CSSJanus() { 95 | 96 | var 97 | // Tokens 98 | temporaryToken = '`TMP`', 99 | temporaryLtrToken = '`TMPLTR`', 100 | temporaryRtlToken = '`TMPRTL`', 101 | noFlipSingleToken = '`NOFLIP_SINGLE`', 102 | noFlipClassToken = '`NOFLIP_CLASS`', 103 | commentToken = '`COMMENT`', 104 | // Patterns 105 | nonAsciiPattern = '[^\\u0020-\\u007e]', 106 | unicodePattern = '(?:(?:\\\\[0-9a-f]{1,6})(?:\\r\\n|\\s)?)', 107 | numPattern = '(?:[0-9]*\\.[0-9]+|[0-9]+)', 108 | unitPattern = '(?:em|ex|px|cm|mm|in|pt|pc|deg|rad|grad|ms|s|hz|khz|%)', 109 | directionPattern = 'direction\\s*:\\s*', 110 | urlSpecialCharsPattern = '[!#$%&*-~]', 111 | validAfterUriCharsPattern = '[\'"]?\\s*', 112 | nonLetterPattern = '(^|[^a-zA-Z])', 113 | charsWithinSelectorPattern = '[^\\}]*?', 114 | noFlipPattern = '\\/\\*\\!?\\s*@noflip\\s*\\*\\/', 115 | commentPattern = '\\/\\*[^*]*\\*+([^\\/*][^*]*\\*+)*\\/', 116 | escapePattern = '(?:' + unicodePattern + '|\\\\[^\\r\\n\\f0-9a-f])', 117 | nmstartPattern = '(?:[_a-z]|' + nonAsciiPattern + '|' + escapePattern + ')', 118 | nmcharPattern = '(?:[_a-z0-9-]|' + nonAsciiPattern + '|' + escapePattern + ')', 119 | identPattern = '-?' + nmstartPattern + nmcharPattern + '*', 120 | quantPattern = numPattern + '(?:\\s*' + unitPattern + '|' + identPattern + ')?', 121 | signedQuantPattern = '((?:-?' + quantPattern + ')|(?:inherit|auto))', 122 | signedQuantSimplePattern = '(?:-?' + numPattern + '(?:\\s*' + unitPattern + ')?)', 123 | mathOperatorsPattern = '(?:\\+|\\-|\\*|\\/)', 124 | allowedCharsPattern = '(?:\\(|\\)|\\t| )', 125 | calcEquationPattern = '(?:' + allowedCharsPattern + '|' + signedQuantSimplePattern + '|' + mathOperatorsPattern + '){3,}', 126 | calcPattern = '(?:calc\\((?:' + calcEquationPattern + ')\\))', 127 | signedQuantCalcPattern = '((?:-?' + quantPattern + ')|(?:inherit|auto)|' + calcPattern + ')', 128 | fourNotationQuantPropsPattern = '((?:margin|padding|border-width)\\s*:\\s*)', 129 | fourNotationColorPropsPattern = '((?:-color|border-style)\\s*:\\s*)', 130 | colorPattern = '(#?' + nmcharPattern + '+|(?:rgba?|hsla?)\\([ \\d.,%-]+\\))', 131 | // The use of a lazy match ("*?") may cause a backtrack limit to be exceeded before finding 132 | // the intended match. This affects 'urlCharsPattern' and 'lookAheadNotOpenBracePattern'. 133 | // We have not yet found this problem on Node.js, but we have on PHP 7, where it was 134 | // mitigated by using a possessive quantifier ("*+"), which are not supported in JS. 135 | // See . 136 | urlCharsPattern = '(?:' + urlSpecialCharsPattern + '|' + nonAsciiPattern + '|' + escapePattern + ')*?', 137 | lookAheadNotLetterPattern = '(?![a-zA-Z])', 138 | lookAheadNotOpenBracePattern = '(?!(' + nmcharPattern + '|\\r?\\n|\\s|#|\\:|\\.|\\,|\\+|>|~|\\(|\\)|\\[|\\]|=|\\*=|~=|\\^=|\'[^\']*\'|"[^"]*"|' + commentToken + ')*?{)', 139 | lookAheadNotClosingParenPattern = '(?!' + urlCharsPattern + validAfterUriCharsPattern + '\\))', 140 | lookAheadForClosingParenPattern = '(?=' + urlCharsPattern + validAfterUriCharsPattern + '\\))', 141 | suffixPattern = '(\\s*(?:!important\\s*)?[;}])', 142 | // Regular expressions 143 | temporaryTokenRegExp = /`TMP`/g, 144 | temporaryLtrTokenRegExp = /`TMPLTR`/g, 145 | temporaryRtlTokenRegExp = /`TMPRTL`/g, 146 | commentRegExp = new RegExp( commentPattern, 'gi' ), 147 | noFlipSingleRegExp = new RegExp( '(' + noFlipPattern + lookAheadNotOpenBracePattern + '[^;}]+;?)', 'gi' ), 148 | noFlipClassRegExp = new RegExp( '(' + noFlipPattern + charsWithinSelectorPattern + '})', 'gi' ), 149 | directionLtrRegExp = new RegExp( '(' + directionPattern + ')ltr', 'gi' ), 150 | directionRtlRegExp = new RegExp( '(' + directionPattern + ')rtl', 'gi' ), 151 | leftRegExp = new RegExp( nonLetterPattern + '(left)' + lookAheadNotLetterPattern + lookAheadNotClosingParenPattern + lookAheadNotOpenBracePattern, 'gi' ), 152 | rightRegExp = new RegExp( nonLetterPattern + '(right)' + lookAheadNotLetterPattern + lookAheadNotClosingParenPattern + lookAheadNotOpenBracePattern, 'gi' ), 153 | leftInUrlRegExp = new RegExp( nonLetterPattern + '(left)' + lookAheadForClosingParenPattern, 'gi' ), 154 | rightInUrlRegExp = new RegExp( nonLetterPattern + '(right)' + lookAheadForClosingParenPattern, 'gi' ), 155 | ltrDirSelector = /(:dir\( *)ltr( *\))/g, 156 | rtlDirSelector = /(:dir\( *)rtl( *\))/g, 157 | ltrInUrlRegExp = new RegExp( nonLetterPattern + '(ltr)' + lookAheadForClosingParenPattern, 'gi' ), 158 | rtlInUrlRegExp = new RegExp( nonLetterPattern + '(rtl)' + lookAheadForClosingParenPattern, 'gi' ), 159 | cursorEastRegExp = new RegExp( nonLetterPattern + '([ns]?)e-resize', 'gi' ), 160 | cursorWestRegExp = new RegExp( nonLetterPattern + '([ns]?)w-resize', 'gi' ), 161 | fourNotationQuantRegExp = new RegExp( fourNotationQuantPropsPattern + signedQuantCalcPattern + '(\\s+)' + signedQuantCalcPattern + '(\\s+)' + signedQuantCalcPattern + '(\\s+)' + signedQuantCalcPattern + suffixPattern, 'gi' ), 162 | fourNotationColorRegExp = new RegExp( fourNotationColorPropsPattern + colorPattern + '(\\s+)' + colorPattern + '(\\s+)' + colorPattern + '(\\s+)' + colorPattern + suffixPattern, 'gi' ), 163 | bgHorizontalPercentageRegExp = new RegExp( '(background(?:-position)?\\s*:\\s*(?:[^:;}\\s]+\\s+)*?)(' + quantPattern + ')', 'gi' ), 164 | bgHorizontalPercentageXRegExp = new RegExp( '(background-position-x\\s*:\\s*)(-?' + numPattern + '%)', 'gi' ), 165 | // border-radius: {1,4} [optional: / {1,4} ] 166 | borderRadiusRegExp = new RegExp( '(border-radius\\s*:\\s*)' + signedQuantPattern + '(?:(?:\\s+' + signedQuantPattern + ')(?:\\s+' + signedQuantPattern + ')?(?:\\s+' + signedQuantPattern + ')?)?' + 167 | '(?:(?:(?:\\s*\\/\\s*)' + signedQuantPattern + ')(?:\\s+' + signedQuantPattern + ')?(?:\\s+' + signedQuantPattern + ')?(?:\\s+' + signedQuantPattern + ')?)?' + suffixPattern, 'gi' ), 168 | boxShadowRegExp = new RegExp( '(box-shadow\\s*:\\s*(?:inset\\s*)?)' + signedQuantPattern, 'gi' ), 169 | textShadow1RegExp = new RegExp( '(text-shadow\\s*:\\s*)' + signedQuantPattern + '(\\s*)' + colorPattern, 'gi' ), 170 | textShadow2RegExp = new RegExp( '(text-shadow\\s*:\\s*)' + colorPattern + '(\\s*)' + signedQuantPattern, 'gi' ), 171 | textShadow3RegExp = new RegExp( '(text-shadow\\s*:\\s*)' + signedQuantPattern, 'gi' ), 172 | translateXRegExp = new RegExp( '(transform\\s*:[^;}]*)(translateX\\s*\\(\\s*)' + signedQuantPattern + '(\\s*\\))', 'gi' ), 173 | translateRegExp = new RegExp( '(transform\\s*:[^;}]*)(translate\\s*\\(\\s*)' + signedQuantPattern + '((?:\\s*,\\s*' + signedQuantPattern + '){0,2}\\s*\\))', 'gi' ); 174 | 175 | /** 176 | * Invert the horizontal value of a background position property. 177 | * 178 | * @private 179 | * @param {string} match Matched property 180 | * @param {string} pre Text before value 181 | * @param {string} value Horizontal value 182 | * @return {string} Inverted property 183 | */ 184 | function calculateNewBackgroundPosition( match, pre, value ) { 185 | var idx, len; 186 | if ( value.slice( -1 ) === '%' ) { 187 | idx = value.indexOf( '.' ); 188 | if ( idx !== -1 ) { 189 | // Two off, one for the "%" at the end, one for the dot itself 190 | len = value.length - idx - 2; 191 | value = 100 - parseFloat( value ); 192 | value = value.toFixed( len ) + '%'; 193 | } else { 194 | value = 100 - parseFloat( value ) + '%'; 195 | } 196 | } 197 | return pre + value; 198 | } 199 | 200 | /** 201 | * Invert a set of border radius values. 202 | * 203 | * @private 204 | * @param {Array} values Matched values 205 | * @return {string} Inverted values 206 | */ 207 | function flipBorderRadiusValues( values ) { 208 | switch ( values.length ) { 209 | case 4: 210 | values = [ values[ 1 ], values[ 0 ], values[ 3 ], values[ 2 ] ]; 211 | break; 212 | case 3: 213 | values = [ values[ 1 ], values[ 0 ], values[ 1 ], values[ 2 ] ]; 214 | break; 215 | case 2: 216 | values = [ values[ 1 ], values[ 0 ] ]; 217 | break; 218 | case 1: 219 | values = [ values[ 0 ] ]; 220 | break; 221 | } 222 | 223 | return values.join( ' ' ); 224 | } 225 | 226 | /** 227 | * Invert a set of border radius values. 228 | * 229 | * @private 230 | * @param {string} match Matched property 231 | * @param {string} pre Text before value 232 | * @param {string} [firstGroup1] 233 | * @param {string} [firstGroup2] 234 | * @param {string} [firstGroup3] 235 | * @param {string} [firstGroup4] 236 | * @param {string} [secondGroup1] 237 | * @param {string} [secondGroup2] 238 | * @param {string} [secondGroup3] 239 | * @param {string} [secondGroup4] 240 | * @param {string} [post] Text after value 241 | * @return {string} Inverted property 242 | */ 243 | function calculateNewBorderRadius( match, pre ) { 244 | var values, 245 | args = [].slice.call( arguments ), 246 | firstGroup = args.slice( 2, 6 ).filter( function ( val ) { 247 | return val; 248 | } ), 249 | secondGroup = args.slice( 6, 10 ).filter( function ( val ) { 250 | return val; 251 | } ), 252 | post = args[ 10 ] || ''; 253 | 254 | if ( secondGroup.length ) { 255 | values = flipBorderRadiusValues( firstGroup ) + ' / ' + flipBorderRadiusValues( secondGroup ); 256 | } else { 257 | values = flipBorderRadiusValues( firstGroup ); 258 | } 259 | 260 | return pre + values + post; 261 | } 262 | 263 | /** 264 | * Flip the sign of a CSS value, possibly with a unit. 265 | * 266 | * We can't just negate the value with unary minus due to the units. 267 | * 268 | * @private 269 | * @param {string} value 270 | * @return {string} 271 | */ 272 | function flipSign( value ) { 273 | if ( parseFloat( value ) === 0 ) { 274 | // Don't mangle zeroes 275 | return value; 276 | } 277 | 278 | if ( value[ 0 ] === '-' ) { 279 | return value.slice( 1 ); 280 | } 281 | 282 | return '-' + value; 283 | } 284 | 285 | /** 286 | * @private 287 | * @param {string} match 288 | * @param {string} property 289 | * @param {string} offset 290 | * @return {string} 291 | */ 292 | function calculateNewShadow( match, property, offset ) { 293 | return property + flipSign( offset ); 294 | } 295 | 296 | /** 297 | * @private 298 | * @param {string} match 299 | * @param {string} property 300 | * @param {string} prefix 301 | * @param {string} offset 302 | * @param {string} suffix 303 | * @return {string} 304 | */ 305 | function calculateNewTranslate( match, property, prefix, offset, suffix ) { 306 | return property + prefix + flipSign( offset ) + suffix; 307 | } 308 | 309 | /** 310 | * @private 311 | * @param {string} match 312 | * @param {string} property 313 | * @param {string} color 314 | * @param {string} space 315 | * @param {string} offset 316 | * @return {string} 317 | */ 318 | function calculateNewFourTextShadow( match, property, color, space, offset ) { 319 | return property + color + space + flipSign( offset ); 320 | } 321 | 322 | return { 323 | /** 324 | * Transform a left-to-right stylesheet to right-to-left. 325 | * 326 | * @param {string} css Stylesheet to transform 327 | * @param {Object} options Options 328 | * @param {boolean} [options.transformDirInUrl=false] Transform directions in URLs 329 | * (e.g. 'ltr', 'rtl') 330 | * @param {boolean} [options.transformEdgeInUrl=false] Transform edges in URLs 331 | * (e.g. 'left', 'right') 332 | * @return {string} Transformed stylesheet 333 | */ 334 | 'transform': function ( css, options ) { // eslint-disable-line quote-props 335 | // Use single quotes in this object literal key for closure compiler. 336 | // Tokenizers 337 | var noFlipSingleTokenizer = new Tokenizer( noFlipSingleRegExp, noFlipSingleToken ), 338 | noFlipClassTokenizer = new Tokenizer( noFlipClassRegExp, noFlipClassToken ), 339 | commentTokenizer = new Tokenizer( commentRegExp, commentToken ); 340 | 341 | // Tokenize 342 | css = commentTokenizer.tokenize( 343 | noFlipClassTokenizer.tokenize( 344 | noFlipSingleTokenizer.tokenize( 345 | // We wrap tokens in ` , not ~ like the original implementation does. 346 | // This was done because ` is not a legal character in CSS and can only 347 | // occur in URLs, where we escape it to %60 before inserting our tokens. 348 | css.replace( '`', '%60' ) 349 | ) 350 | ) 351 | ); 352 | 353 | // Transform URLs 354 | if ( options.transformDirInUrl ) { 355 | // Replace 'ltr' with 'rtl' and vice versa in background URLs 356 | css = css 357 | .replace( ltrDirSelector, '$1' + temporaryLtrToken + '$2' ) 358 | .replace( rtlDirSelector, '$1' + temporaryRtlToken + '$2' ) 359 | .replace( ltrInUrlRegExp, '$1' + temporaryToken ) 360 | .replace( rtlInUrlRegExp, '$1ltr' ) 361 | .replace( temporaryTokenRegExp, 'rtl' ) 362 | .replace( temporaryLtrTokenRegExp, 'ltr' ) 363 | .replace( temporaryRtlTokenRegExp, 'rtl' ); 364 | } 365 | if ( options.transformEdgeInUrl ) { 366 | // Replace 'left' with 'right' and vice versa in background URLs 367 | css = css 368 | .replace( leftInUrlRegExp, '$1' + temporaryToken ) 369 | .replace( rightInUrlRegExp, '$1left' ) 370 | .replace( temporaryTokenRegExp, 'right' ); 371 | } 372 | 373 | // Transform rules 374 | css = css 375 | // Replace direction: ltr; with direction: rtl; and vice versa. 376 | .replace( directionLtrRegExp, '$1' + temporaryToken ) 377 | .replace( directionRtlRegExp, '$1ltr' ) 378 | .replace( temporaryTokenRegExp, 'rtl' ) 379 | // Flip rules like left: , padding-right: , etc. 380 | .replace( leftRegExp, '$1' + temporaryToken ) 381 | .replace( rightRegExp, '$1left' ) 382 | .replace( temporaryTokenRegExp, 'right' ) 383 | // Flip East and West in rules like cursor: nw-resize; 384 | .replace( cursorEastRegExp, '$1$2' + temporaryToken ) 385 | .replace( cursorWestRegExp, '$1$2e-resize' ) 386 | .replace( temporaryTokenRegExp, 'w-resize' ) 387 | // Border radius 388 | .replace( borderRadiusRegExp, calculateNewBorderRadius ) 389 | // Shadows 390 | .replace( boxShadowRegExp, calculateNewShadow ) 391 | .replace( textShadow1RegExp, calculateNewFourTextShadow ) 392 | .replace( textShadow2RegExp, calculateNewFourTextShadow ) 393 | .replace( textShadow3RegExp, calculateNewShadow ) 394 | // Translate 395 | .replace( translateXRegExp, calculateNewTranslate ) 396 | .replace( translateRegExp, calculateNewTranslate ) 397 | // Swap the second and fourth parts in four-part notation rules 398 | // like padding: 1px 2px 3px 4px; 399 | .replace( fourNotationQuantRegExp, '$1$2$3$8$5$6$7$4$9' ) 400 | .replace( fourNotationColorRegExp, '$1$2$3$8$5$6$7$4$9' ) 401 | // Flip horizontal background percentages 402 | .replace( bgHorizontalPercentageRegExp, calculateNewBackgroundPosition ) 403 | .replace( bgHorizontalPercentageXRegExp, calculateNewBackgroundPosition ); 404 | 405 | // Detokenize 406 | css = noFlipSingleTokenizer.detokenize( 407 | noFlipClassTokenizer.detokenize( 408 | commentTokenizer.detokenize( css ) 409 | ) 410 | ); 411 | 412 | return css; 413 | } 414 | }; 415 | } 416 | 417 | /* Initialization */ 418 | 419 | cssjanus = new CSSJanus(); 420 | 421 | /* Exports */ 422 | 423 | if ( typeof module !== 'undefined' && module.exports ) { 424 | /** 425 | * Transform a left-to-right stylesheet to right-to-left. 426 | * 427 | * This function is a static wrapper around the transform method of an instance of CSSJanus. 428 | * 429 | * @param {string} css Stylesheet to transform 430 | * @param {Object|boolean} [options] Options object, or transformDirInUrl option (back-compat) 431 | * @param {boolean} [options.transformDirInUrl=false] Transform directions in URLs 432 | * (e.g. 'ltr', 'rtl') 433 | * @param {boolean} [options.transformEdgeInUrl=false] Transform edges in URLs 434 | * (e.g. 'left', 'right') 435 | * @param {boolean} [transformEdgeInUrl] Back-compat parameter 436 | * @return {string} Transformed stylesheet 437 | */ 438 | exports.transform = function ( css, options, transformEdgeInUrl ) { 439 | var norm; 440 | if ( typeof options === 'object' ) { 441 | norm = options; 442 | } else { 443 | norm = {}; 444 | if ( typeof options === 'boolean' ) { 445 | norm.transformDirInUrl = options; 446 | } 447 | if ( typeof transformEdgeInUrl === 'boolean' ) { 448 | norm.transformEdgeInUrl = transformEdgeInUrl; 449 | } 450 | } 451 | return cssjanus.transform( css, norm ); 452 | }; 453 | } else if ( typeof window !== 'undefined' ) { 454 | /* global window */ 455 | // Allow cssjanus to be used in a browser. 456 | // eslint-disable-next-line dot-notation 457 | window[ 'cssjanus' ] = cssjanus; 458 | } 459 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "wikimedia", 5 | "wikimedia/node", 6 | "wikimedia/language/es2021" 7 | ], 8 | "rules": { 9 | "n/no-process-exit": "warn" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/bench.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint no-process-exit:"off" */ 3 | 4 | const crypto = require( 'crypto' ); 5 | const fs = require( 'fs' ); 6 | const https = require( 'https' ); 7 | const cssjanus = require( '../' ); 8 | 9 | const baseBench = { 10 | name: '', 11 | started: NaN, 12 | start: function ( name ) { 13 | this.name = name; 14 | this.started = process.hrtime(); 15 | }, 16 | end: function ( ops ) { 17 | const elapsed = process.hrtime( this.started ); 18 | if ( elapsed[ 0 ] === 0 && elapsed[ 1 ] === 0 ) { 19 | throw new Error( 'insufficient clock precision for short benchmark' ); 20 | } 21 | const time = elapsed[ 0 ] + elapsed[ 1 ] / 1e9; 22 | const rate = ops / time; 23 | this.report( rate, time ); 24 | }, 25 | report: function ( rate, time ) { 26 | console.log( 'Bench ' + this.name + 27 | ': ' + rate.toFixed( 0 ) + ' op/s in ' + 28 | time.toFixed( 1 ) + 's' 29 | ); 30 | } 31 | }; 32 | 33 | function checksum( algorithm, str ) { 34 | return crypto 35 | .createHash( algorithm ) 36 | .update( str, 'utf8' ) 37 | .digest( 'hex' ); 38 | } 39 | 40 | function fetch( url ) { 41 | let redirects = 0; 42 | return new Promise( ( resolve, reject ) => { 43 | https.get( url, function handleResponse( res ) { 44 | let data = ''; 45 | // Handle redirect 46 | if ( res.statusCode === 301 || res.statusCode === 302 ) { 47 | if ( !res.headers.location ) { 48 | reject( new Error( 'Redirect without location' ) ); 49 | return; 50 | } 51 | redirects++; 52 | if ( redirects > 1 ) { 53 | reject( new Error( 'Too many redirects' ) ); 54 | return; 55 | } 56 | https.get( res.headers.location, handleResponse ); 57 | res.resume(); 58 | return; 59 | } 60 | // Handle http error 61 | if ( res.statusCode !== 200 ) { 62 | reject( new Error( 'HTTP ' + res.statusCode ) ); 63 | res.resume(); 64 | return; 65 | } 66 | res.setEncoding( 'utf8' ); 67 | res.on( 'data', ( chunk ) => { 68 | data += chunk; 69 | } ); 70 | res.on( 'end', () => { 71 | resolve( data ); 72 | } ); 73 | } ).on( 'error', ( err ) => { 74 | reject( err ); 75 | } ); 76 | } ); 77 | } 78 | 79 | async function getFixture( name, sha1, url ) { 80 | const file = __dirname + '/fixture.' + name + '.dat'; 81 | let data; 82 | 83 | try { 84 | data = fs.readFileSync( file, 'utf8' ); 85 | if ( checksum( 'sha1', data ) === sha1 ) { 86 | return data; 87 | } 88 | } catch ( e ) { 89 | // Ignore 90 | } 91 | 92 | data = await fetch( url ); 93 | data = Buffer.from( data, 'base64' ).toString( 'utf8' ); 94 | 95 | if ( checksum( 'sha1', data ) !== sha1 ) { 96 | throw new Error( 'Checksum mis-match' ); 97 | } 98 | 99 | fs.writeFileSync( file, data ); 100 | 101 | return data; 102 | } 103 | 104 | async function benchFixture( fixture ) { 105 | const data = await getFixture( fixture.name, fixture.sha1, fixture.src ); 106 | const ops = 10_000; 107 | const bench = Object.create( baseBench ); 108 | let i = ops; 109 | bench.start( fixture.name ); 110 | while ( i-- ) { 111 | cssjanus.transform( data ); 112 | } 113 | bench.end( ops ); 114 | } 115 | 116 | async function main() { 117 | const fixtures = [ 118 | { 119 | name: 'mediawiki', 120 | sha1: '6277eb6b3ce25e2abcaa720f5da1b979686ea166', 121 | src: 'https://gerrit.wikimedia.org/g/mediawiki/core/+/10644263276ab941b19d2365e16813bd57e9d1f5/resources/src/mediawiki.legacy/shared.css?format=TEXT' 122 | }, 123 | { 124 | name: 'ooui', 125 | sha1: 'b6f7ebc0e26c53617284d3f3a99552f9ffbf85fa', 126 | src: 'https://gerrit.wikimedia.org/g/mediawiki/core/+/130344b47ad939114400d2d0dfbc4018d6d2b5a9/resources/lib/oojs-ui/oojs-ui-core-wikimediaui.css?format=TEXT' 127 | } 128 | ]; 129 | for ( const fixture of fixtures ) { 130 | await benchFixture( fixture ); 131 | } 132 | } 133 | 134 | main().catch( ( err ) => { 135 | console.error( err ); 136 | process.exit( 1 ); 137 | } ); 138 | -------------------------------------------------------------------------------- /test/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "preserve comments": { 3 | "cases": [ 4 | [ 5 | "/* left /* right */left: 10px", 6 | "/* left /* right */right: 10px" 7 | ], 8 | [ 9 | "/*left*//*left*/left: 10px", 10 | "/*left*//*left*/right: 10px" 11 | ], 12 | [ 13 | "/* Going right is cool */\n#test {left: 10px}", 14 | "/* Going right is cool */\n#test {right: 10px}" 15 | ], 16 | [ 17 | "/* padding-right 1 2 3 4 */\n#test {left: 10px}\n/*right*/", 18 | "/* padding-right 1 2 3 4 */\n#test {right: 10px}\n/*right*/" 19 | ], 20 | [ 21 | "/** Two line comment\n * left\n \\*/\n#test {left: 10px}", 22 | "/** Two line comment\n * left\n \\*/\n#test {right: 10px}" 23 | ] 24 | ] 25 | }, 26 | "flip position": { 27 | "cases": [ 28 | [ 29 | ".foo { left: 10px; }", 30 | ".foo { right: 10px; }" 31 | ], 32 | [ 33 | ".foo { left: 10px !important; }", 34 | ".foo { right: 10px !important; }" 35 | ] 36 | ] 37 | }, 38 | "flip negative values": { 39 | "cases": [ 40 | [ 41 | ".foo { left:-1.5em; }", 42 | ".foo { right:-1.5em; }" 43 | ], 44 | [ 45 | ".foo { left:-.75em; }", 46 | ".foo { right:-.75em; }" 47 | ], 48 | [ 49 | ".foo { padding: 1px 2px 3px -4px; }", 50 | ".foo { padding: 1px -4px 3px 2px; }" 51 | ] 52 | ] 53 | }, 54 | "flip four value notation": { 55 | "cases": [ 56 | [ 57 | ".foo { padding: .25em 0ex 0pt 15px; }", 58 | ".foo { padding: .25em 15px 0pt 0ex; }" 59 | ], 60 | [ 61 | ".foo { padding: 1px 2% 3px 4.1grad; }", 62 | ".foo { padding: 1px 4.1grad 3px 2%; }" 63 | ], 64 | [ 65 | ".foo { padding: 1px auto 3px 2px; }", 66 | ".foo { padding: 1px 2px 3px auto; }" 67 | ], 68 | [ 69 | ".foo { padding: 1.1px 2.2px 3.3px 4.4px; }", 70 | ".foo { padding: 1.1px 4.4px 3.3px 2.2px; }" 71 | ], 72 | [ 73 | ".foo { padding: 1px auto 3px inherit; }", 74 | ".foo { padding: 1px inherit 3px auto; }" 75 | ], 76 | [ 77 | ".foo { padding: 1px 2px 3px 4px !important; }", 78 | ".foo { padding: 1px 4px 3px 2px !important; }" 79 | ], 80 | [ 81 | ".foo {padding:1px 2px 3px 4px !important}", 82 | ".foo {padding:1px 4px 3px 2px !important}" 83 | ], 84 | [ 85 | ".foo { padding: 1px 2px 3px 4px !important; color: red; }", 86 | ".foo { padding: 1px 4px 3px 2px !important; color: red; }" 87 | ], 88 | [ 89 | ".foo{padding:1px 2px 3px 4px}", 90 | ".foo{padding:1px 4px 3px 2px}" 91 | ], 92 | [ 93 | " .foo { padding: 1px 2px 3px 4px ; } ", 94 | " .foo { padding: 1px 4px 3px 2px ; } " 95 | ], 96 | [ 97 | "#settings td p strong {}" 98 | ], 99 | [ 100 | ".foo barpx 0 2% { opacity: 0; }" 101 | ], 102 | [ 103 | ".foo { -x-unknown: a b c d; }" 104 | ], 105 | [ 106 | ".foo { -x-unknown: 1px 2px 3px 4px; }" 107 | ], 108 | [ 109 | ".foo { -x-unknown: 1px 2px 3px 4px 5px; }" 110 | ], 111 | [ 112 | ".foo { padding: 1px calc( 2 * 4px ) 3px 4px !important; color: red; }", 113 | ".foo { padding: 1px 4px 3px calc( 2 * 4px ) !important; color: red; }" 114 | ], 115 | [ 116 | ".foo { padding: 1px calc( ( 1 + 1 ) * 1px ) 3px 4px; }", 117 | ".foo { padding: 1px 4px 3px calc( ( 1 + 1 ) * 1px ); }" 118 | ], 119 | [ 120 | ".bar { padding: 1px 4px 3px calc( 1 * 1px ); content: attr(data-title); }", 121 | ".bar { padding: 1px calc( 1 * 1px ) 3px 4px; content: attr(data-title); }" 122 | ], 123 | [ 124 | ".bar { padding: 1px 4px 3px calc( 10 / 5 * 1px ); content: attr(data-title); }", 125 | ".bar { padding: 1px calc( 10 / 5 * 1px ) 3px 4px; content: attr(data-title); }" 126 | ], 127 | [ 128 | ".foo { padding: 1px calc( ( 1 + 1 ) * 1px - ( 1 / 1 ) ) 3px 4px; }", 129 | ".foo { padding: 1px 4px 3px calc( ( 1 + 1 ) * 1px - ( 1 / 1 ) ); }" 130 | ] 131 | ] 132 | }, 133 | "flip direction": { 134 | "cases": [ 135 | [ 136 | ".foo { direction: ltr; }", 137 | ".foo { direction: rtl; }" 138 | ], 139 | [ 140 | "input { direction: rtl; }", 141 | "input { direction: ltr; }" 142 | ], 143 | [ 144 | "body { direction: rtl; }", 145 | "body { direction: ltr; }" 146 | ], 147 | [ 148 | "body { padding: 10px; direction: rtl; }", 149 | "body { padding: 10px; direction: ltr; }" 150 | ], 151 | [ 152 | ".foo, body, input { direction: rtl; }", 153 | ".foo, body, input { direction: ltr; }" 154 | ], 155 | [ 156 | "body { direction: rtl; } .myClass { direction: ltr; }", 157 | "body { direction: ltr; } .myClass { direction: rtl; }" 158 | ], 159 | [ 160 | "body{\n direction: rtl\n}", 161 | "body{\n direction: ltr\n}" 162 | ] 163 | ] 164 | }, 165 | "flip float": { 166 | "cases": [ 167 | [ 168 | ".foo { float: left; }", 169 | ".foo { float: right; }" 170 | ], 171 | [ 172 | ".foo { float: left !important; }", 173 | ".foo { float: right !important; }" 174 | ], 175 | [ 176 | ".foo { clear: left; }", 177 | ".foo { clear: right; }" 178 | ] 179 | ] 180 | }, 181 | "flip padding": { 182 | "cases": [ 183 | [ 184 | ".foo { padding: 1px; }" 185 | ], 186 | [ 187 | ".foo { padding: 1px 2px; }" 188 | ], 189 | [ 190 | ".foo { padding: 1px 2px 3px; }" 191 | ], 192 | [ 193 | ".foo { padding: 1px 2px 3px 4px; }", 194 | ".foo { padding: 1px 4px 3px 2px; }" 195 | ], 196 | [ 197 | ".foo { padding: 1px 2px 3px 4px 5px; }" 198 | ], 199 | [ 200 | ".foo { padding: 1px 2px 3px 4px 5px 6px; }" 201 | ] 202 | ] 203 | }, 204 | "flip padding-{edge}": { 205 | "cases": [ 206 | [ 207 | ".foo { padding-left: 0; }", 208 | ".foo { padding-right: 0; }" 209 | ] 210 | ] 211 | }, 212 | "flip margin-{edge}": { 213 | "cases": [ 214 | [ 215 | ".foo { margin-left: 0; }", 216 | ".foo { margin-right: 0; }" 217 | ] 218 | ] 219 | }, 220 | "flip cursor": { 221 | "cases": [ 222 | [ 223 | ".foo { cursor: w-resize; }", 224 | ".foo { cursor: e-resize; }" 225 | ], 226 | [ 227 | ".foo { cursor: sw-resize; }", 228 | ".foo { cursor: se-resize; }" 229 | ], 230 | [ 231 | ".foo { cursor: nw-resize; }", 232 | ".foo { cursor: ne-resize; }" 233 | ] 234 | ] 235 | }, 236 | "flip text-align": { 237 | "cases": [ 238 | [ 239 | ".foo { text-align: left; }", 240 | ".foo { text-align: right; }" 241 | ] 242 | ] 243 | }, 244 | "flip text-shadow": { 245 | "cases": [ 246 | [ 247 | ".foo { text-shadow: 1px 2px 3px red; }", 248 | ".foo { text-shadow: -1px 2px 3px red; }" 249 | ], 250 | [ 251 | ".foo { text-shadow: red 1px 2px 3px; }", 252 | ".foo { text-shadow: red -1px 2px 3px; }" 253 | ], 254 | [ 255 | ".foo { text-shadow: 1px 2px red; }", 256 | ".foo { text-shadow: -1px 2px red; }" 257 | ], 258 | [ 259 | ".foo { text-shadow: red 1px 2px; }", 260 | ".foo { text-shadow: red -1px 2px; }" 261 | ], 262 | [ 263 | ".foo { text-shadow: 1px 2px; }", 264 | ".foo { text-shadow: -1px 2px; }" 265 | ], 266 | [ 267 | ".foo { text-shadow: red 0 2px; }" 268 | ] 269 | ] 270 | }, 271 | "flip box-shadow": { 272 | "cases": [ 273 | [ 274 | ".foo { box-shadow: -6px 3px 8px 5px rgba(0, 0, 0, 0.25); }", 275 | ".foo { box-shadow: 6px 3px 8px 5px rgba(0, 0, 0, 0.25); }" 276 | ], 277 | [ 278 | ".foo { box-shadow: inset -6px 3px 8px 5px rgba(0, 0, 0, 0.25); }", 279 | ".foo { box-shadow: inset 6px 3px 8px 5px rgba(0, 0, 0, 0.25); }" 280 | ], 281 | [ 282 | ".foo { box-shadow: inset .5em 0 0 white; }", 283 | ".foo { box-shadow: inset -.5em 0 0 white; }" 284 | ], 285 | [ 286 | ".foo { box-shadow: inset 0.5em 0 0 white; }", 287 | ".foo { box-shadow: inset -0.5em 0 0 white; }" 288 | ], 289 | [ 290 | ".foo { box-shadow: none; }" 291 | ], 292 | [ 293 | ".foo { -webkit-box-shadow: -1px 2px 3px 3px red; }", 294 | ".foo { -webkit-box-shadow: 1px 2px 3px 3px red; }" 295 | ], 296 | [ 297 | ".foo { -moz-box-shadow: -1px 2px 3px 3px red; }", 298 | ".foo { -moz-box-shadow: 1px 2px 3px 3px red; }" 299 | ], 300 | [ 301 | ".foo{box-shadow:-1px 2px 3px 3px red}", 302 | ".foo{box-shadow:1px 2px 3px 3px red}" 303 | ], 304 | [ 305 | ".foo { box-shadow: -1px 2px 3px 3px red ; }", 306 | ".foo { box-shadow: 1px 2px 3px 3px red ; }" 307 | ] 308 | ] 309 | }, 310 | "flip border-{edge}": { 311 | "cases": [ 312 | [ 313 | ".foo { border-left: 0; }", 314 | ".foo { border-right: 0; }" 315 | ], 316 | [ 317 | ".foo { border-left: 1px solid red; }", 318 | ".foo { border-right: 1px solid red; }" 319 | ] 320 | ] 321 | }, 322 | "flip border-{edge}-color": { 323 | "cases": [ 324 | [ 325 | ".foo { border-left-color: red; }", 326 | ".foo { border-right-color: red; }" 327 | ] 328 | ] 329 | }, 330 | "flip border-{edge}-style": { 331 | "cases": [ 332 | [ 333 | ".foo { border-left-style: red; }", 334 | ".foo { border-right-style: red; }" 335 | ] 336 | ] 337 | }, 338 | "flip border-color": { 339 | "cases": [ 340 | [ 341 | ".foo { border-color: red green blue white; }", 342 | ".foo { border-color: red white blue green; }" 343 | ], 344 | [ 345 | ".foo { border-color: red #f00 rgb(255, 0, 0) rgba(255, 0, 0, 0.5); }", 346 | ".foo { border-color: red rgba(255, 0, 0, 0.5) rgb(255, 0, 0) #f00; }" 347 | ], 348 | [ 349 | ".foo { border-color: red #f00 hsl(0, 100%, 50%) hsla(0, 100%, 50%, 0.5); }", 350 | ".foo { border-color: red hsla(0, 100%, 50%, 0.5) hsl(0, 100%, 50%) #f00; }" 351 | ] 352 | ] 353 | }, 354 | "flip border-width": { 355 | "cases": [ 356 | [ 357 | ".foo { border-width: 1px 2px 3px 4px; }", 358 | ".foo { border-width: 1px 4px 3px 2px; }" 359 | ] 360 | ] 361 | }, 362 | "flip border-style": { 363 | "cases": [ 364 | [ 365 | ".foo { border-style: none dotted dashed solid; }", 366 | ".foo { border-style: none solid dashed dotted; }" 367 | ] 368 | ] 369 | }, 370 | "flip border-radius": { 371 | "cases": [ 372 | [ 373 | ".foo { border-radius: 1px; }" 374 | ], 375 | [ 376 | ".foo { border-radius: 1px 2px; }", 377 | ".foo { border-radius: 2px 1px; }" 378 | ], 379 | [ 380 | ".foo { border-radius: 1px 2px 3px 4px; }", 381 | ".foo { border-radius: 2px 1px 4px 3px; }" 382 | ], 383 | [ 384 | ".foo{border-radius:1px 2px 3px 4px}", 385 | ".foo{border-radius:2px 1px 4px 3px}" 386 | ], 387 | [ 388 | ".foo{ border-radius: 10px / 20px }" 389 | ], 390 | [ 391 | ".foo{ border-radius: 15px / 0 20px }", 392 | ".foo{ border-radius: 15px / 20px 0 }" 393 | ], 394 | [ 395 | ".foo{ border-radius: 1px 2px 3px 4px / 5px 6px 7px 8px }", 396 | ".foo{ border-radius: 2px 1px 4px 3px / 6px 5px 8px 7px }" 397 | ], 398 | [ 399 | ".foo{ border-radius: 0 !important }" 400 | ], 401 | [ 402 | ".foo{ border-radius: 1px 2px 3px 4px !important; }", 403 | ".foo{ border-radius: 2px 1px 4px 3px !important; }" 404 | ], 405 | [ 406 | ".foo { border-radius: 1px 2px 3px 4px ; }", 407 | ".foo { border-radius: 2px 1px 4px 3px ; }" 408 | ], 409 | [ 410 | ".foo { border-radius: 1px 2px 3px 4px 5px; }" 411 | ] 412 | ] 413 | }, 414 | "flip border-radius (one-way)": { 415 | "roundtrip": false, 416 | "cases": [ 417 | [ 418 | ".foo { border-radius: 1px 2px 3px; }", 419 | ".foo { border-radius: 2px 1px 2px 3px; }" 420 | ] 421 | ] 422 | }, 423 | "flip border-top-{edge}-radius": { 424 | "cases": [ 425 | [ 426 | ".foo { border-top-left-radius: 0; }", 427 | ".foo { border-top-right-radius: 0; }" 428 | ] 429 | ] 430 | }, 431 | "flip border-bottom-{edge}-radius": { 432 | "cases": [ 433 | [ 434 | ".foo { border-bottom-left-radius: 0; }", 435 | ".foo { border-bottom-right-radius: 0; }" 436 | ] 437 | ] 438 | }, 439 | "flip transform translate x-axis": { 440 | "cases": [ 441 | [ 442 | ".foo { transform: translate( 30px ); }", 443 | ".foo { transform: translate( -30px ); }" 444 | ], 445 | [ 446 | ".foo { transform: translate( 30% ); }", 447 | ".foo { transform: translate( -30% ); }" 448 | ], 449 | [ 450 | ".foo { transform: translate( 30%, 20% ); }", 451 | ".foo { transform: translate( -30%, 20% ); }" 452 | ], 453 | [ 454 | ".foo { transform: translate( 30%, 20%, 10% ); }", 455 | ".foo { transform: translate( -30%, 20%, 10% ); }" 456 | ], 457 | [ 458 | ".foo { transform: translate( 30%, 20%, 10%, 0% ); }" 459 | ], 460 | [ 461 | ".foo { transform: translateY( 30px ) rotate( 20deg ) translateX( 10px ); }", 462 | ".foo { transform: translateY( 30px ) rotate( 20deg ) translateX( -10px ); }" 463 | ], 464 | [ 465 | ".foo { transform: translateX( 30px ) rotate( 20deg ) translateY( 10px ); }", 466 | ".foo { transform: translateX( -30px ) rotate( 20deg ) translateY( 10px ); }" 467 | ], 468 | [ 469 | ".foo { transform: translateX( 30px ); }", 470 | ".foo { transform: translateX( -30px ); }" 471 | ], 472 | [ 473 | ".foo { other-property: translateX( 30px ); }" 474 | ], 475 | [ 476 | ".foo { -webkit-transform: translateX( 30px ); }", 477 | ".foo { -webkit-transform: translateX( -30px ); }" 478 | ], 479 | [ 480 | ".foo { transform: translateY( 30px ); }" 481 | ], 482 | [ 483 | ".foo { transform: translate( 10px ) } .bar { transform: translate( 10px ) }", 484 | ".foo { transform: translate( -10px ) } .bar { transform: translate( -10px ) }" 485 | ] 486 | ] 487 | }, 488 | "flip background-position keywords": { 489 | "cases": [ 490 | [ 491 | ".foo { background-position: left top; }", 492 | ".foo { background-position: right top; }" 493 | ], 494 | [ 495 | ".foo { background: url(/foo/bar.png) left top; }", 496 | ".foo { background: url(/foo/bar.png) right top; }" 497 | ], 498 | [ 499 | ".foo { background: url(/foo/bar.png) no-repeat left top; }", 500 | ".foo { background: url(/foo/bar.png) no-repeat right top; }" 501 | ], 502 | [ 503 | ".foo { background: #000 url(/foo/bar.png) no-repeat left top; }", 504 | ".foo { background: #000 url(/foo/bar.png) no-repeat right top; }" 505 | ], 506 | [ 507 | ".foo { background-position: left -5px; }", 508 | ".foo { background-position: right -5px; }" 509 | ] 510 | ] 511 | }, 512 | "flip background-position percentages": { 513 | "cases": [ 514 | [ 515 | ".foo { background-position: 77% 40%; }", 516 | ".foo { background-position: 23% 40%; }" 517 | ], 518 | [ 519 | ".foo { background-position: 2.3% 40%; }", 520 | ".foo { background-position: 97.7% 40%; }" 521 | ], 522 | [ 523 | ".foo { background-position: 2.3210% 40%; }", 524 | ".foo { background-position: 97.6790% 40%; }" 525 | ], 526 | [ 527 | ".foo { background-position: 0% 100%; }", 528 | ".foo { background-position: 100% 100%; }" 529 | ], 530 | [ 531 | ".foo { background-position: 77% -5px; }", 532 | ".foo { background-position: 23% -5px; }" 533 | ], 534 | [ 535 | ".foo { background-position: 0% 100% !important; }", 536 | ".foo { background-position: 100% 100% !important; }" 537 | ], 538 | [ 539 | ".foo{background-position: 0% 100%}", 540 | ".foo{background-position: 100% 100%}" 541 | ], 542 | [ 543 | ".foo { background-position: 0% 100% ; }", 544 | ".foo { background-position: 100% 100% ; }" 545 | ] 546 | ] 547 | }, 548 | "do not flip background-position non-percentages": { 549 | "cases": [ 550 | [ 551 | ".foo { background-position: 0 5px; }" 552 | ], 553 | [ 554 | ".foo { background-position: 10px 20px; }" 555 | ], 556 | [ 557 | ".foo { background-position: 10px 40%; }" 558 | ], 559 | [ 560 | ".foo { background-position: 10px 2.3%; }" 561 | ] 562 | ] 563 | }, 564 | "flip background percentages": { 565 | "cases": [ 566 | [ 567 | ".foo { background: url(/foo/bar.png) 77% 40%; }", 568 | ".foo { background: url(/foo/bar.png) 23% 40%; }" 569 | ], 570 | [ 571 | ".foo { background: url(/foo/bar.png) 77%; }", 572 | ".foo { background: url(/foo/bar.png) 23%; }" 573 | ], 574 | [ 575 | ".foo { background: url(/foo/bar.png) no-repeat 77% 40%; }", 576 | ".foo { background: url(/foo/bar.png) no-repeat 23% 40%; }" 577 | ], 578 | [ 579 | ".foo { background: #000 url(/foo/bar.png) no-repeat 77% 40%; }", 580 | ".foo { background: #000 url(/foo/bar.png) no-repeat 23% 40%; }" 581 | ], 582 | [ 583 | ".foo { background: #000 url(/foo/bar.png) no-repeat 77% 40%; }", 584 | ".foo { background: #000 url(/foo/bar.png) no-repeat 23% 40%; }" 585 | ], 586 | [ 587 | ".foo { background: 77% 40%; } .bar { background: 0% 100%; }", 588 | ".foo { background: 23% 40%; } .bar { background: 100% 100%; }" 589 | ], 590 | [ 591 | ".foo { background: url(/foo/bar.png) 77% 40% !important; }", 592 | ".foo { background: url(/foo/bar.png) 23% 40% !important; }" 593 | ], 594 | [ 595 | ".foo{background:url(/foo/bar.png) 77% 40%}", 596 | ".foo{background:url(/foo/bar.png) 23% 40%}" 597 | ], 598 | [ 599 | ".foo { background: url(/foo/bar.png) 77% 40% ; }", 600 | ".foo { background: url(/foo/bar.png) 23% 40% ; }" 601 | ] 602 | ] 603 | }, 604 | "flip background-position-x percentages": { 605 | "cases": [ 606 | [ 607 | ".foo { background-position-x: 77%; }", 608 | ".foo { background-position-x: 23%; }" 609 | ], 610 | [ 611 | ".foo { background-position-x: 77% !important; }", 612 | ".foo { background-position-x: 23% !important; }" 613 | ], 614 | [ 615 | ".foo{background-position-x:77%}", 616 | ".foo{background-position-x:23%}" 617 | ], 618 | [ 619 | ".foo { background-position-x: 77% ; }", 620 | ".foo { background-position-x: 23% ; }" 621 | ] 622 | ] 623 | }, 624 | "do not flip background-position-y": { 625 | "cases": [ 626 | [ 627 | ".foo { background-position-y: 40%; }" 628 | ] 629 | ] 630 | }, 631 | "do not flip URLs when url transforms are off": { 632 | "options": { 633 | "transformDirInUrl": false, 634 | "transformEdgeInUrl": false 635 | }, 636 | "cases": [ 637 | [ 638 | "background: url(/foo/bar-left.png)" 639 | ], 640 | [ 641 | "background: url(/foo/left-bar.png)" 642 | ], 643 | [ 644 | "url(\"http://www.blogger.com/img/triangle_ltr.gif\")" 645 | ], 646 | [ 647 | "url('http://www.blogger.com/img/triangle_ltr.gif')" 648 | ], 649 | [ 650 | "url('http://www.blogger.com/img/triangle_ltr.gif' )" 651 | ], 652 | [ 653 | "background: url(/foo/bar.left.png)" 654 | ], 655 | [ 656 | "background: url(/foo/bar-rtl.png)" 657 | ], 658 | [ 659 | "background: url(/foo/bar-rtl.png); right: 10px", 660 | "background: url(/foo/bar-rtl.png); left: 10px" 661 | ], 662 | [ 663 | "background: url(/foo/bar-right.png); direction: ltr", 664 | "background: url(/foo/bar-right.png); direction: rtl" 665 | ], 666 | [ 667 | "background: url(/foo/bar-rtl_right.png);right:10px; direction: ltr", 668 | "background: url(/foo/bar-rtl_right.png);left:10px; direction: rtl" 669 | ] 670 | ] 671 | }, 672 | "flip URLs when url transforms are on": { 673 | "options": { 674 | "transformDirInUrl": true, 675 | "transformEdgeInUrl": true 676 | }, 677 | "cases": [ 678 | [ 679 | "background: url(/foo/bar-right.png)", 680 | "background: url(/foo/bar-left.png)" 681 | ], 682 | [ 683 | "background: url(/foo/right-bar.png)", 684 | "background: url(/foo/left-bar.png)" 685 | ], 686 | [ 687 | "url(\"http://www.blogger.com/img/triangle_rtl.gif\")", 688 | "url(\"http://www.blogger.com/img/triangle_ltr.gif\")" 689 | ], 690 | [ 691 | "url('http://www.blogger.com/img/triangle_rtl.gif')", 692 | "url('http://www.blogger.com/img/triangle_ltr.gif')" 693 | ], 694 | [ 695 | "url('http://www.blogger.com/img/triangle_rtl.gif'\t)", 696 | "url('http://www.blogger.com/img/triangle_ltr.gif'\t)" 697 | ], 698 | [ 699 | "background: url(/foo/bar.right.png)", 700 | "background: url(/foo/bar.left.png)" 701 | ], 702 | [ 703 | "background: url(/foo/bright.png)" 704 | ], 705 | [ 706 | "background: url(/foo/bar-ltr.png)", 707 | "background: url(/foo/bar-rtl.png)" 708 | ], 709 | [ 710 | "background: url(/foo/bar-ltr.png); right: 10px", 711 | "background: url(/foo/bar-rtl.png); left: 10px" 712 | ], 713 | [ 714 | "background: url(/foo/bar-left.png); direction: ltr", 715 | "background: url(/foo/bar-right.png); direction: rtl" 716 | ], 717 | [ 718 | "background: url(/foo/bar-ltr_left.png);right:10px; direction: ltr", 719 | "background: url(/foo/bar-rtl_right.png);left:10px; direction: rtl" 720 | ] 721 | ] 722 | }, 723 | "do not flip URLs (back-compat boolean argument)": { 724 | "args": [ 725 | false, 726 | false 727 | ], 728 | "cases": [ 729 | [ 730 | "background: url(/foo/bar-ltr_left.png);right:10px; direction: ltr", 731 | "background: url(/foo/bar-ltr_left.png);left:10px; direction: rtl" 732 | ] 733 | ] 734 | }, 735 | "flip URLs (back-compat boolean argument)": { 736 | "args": [ 737 | true, 738 | true 739 | ], 740 | "cases": [ 741 | [ 742 | "background: url(/foo/bar-ltr_left.png);right:10px; direction: ltr", 743 | "background: url(/foo/bar-rtl_right.png);left:10px; direction: rtl" 744 | ] 745 | ] 746 | }, 747 | "leave class names alone": { 748 | "cases": [ 749 | [ 750 | ".x-left { width: 0; }" 751 | ], 752 | [ 753 | "#bright-left { width: 0; }" 754 | ], 755 | [ 756 | "div.left:hover { width: 0; }" 757 | ], 758 | [ 759 | "#bright-left,\n.foo { width: 0; }" 760 | ], 761 | [ 762 | "#bright-left, .foo { width: 0; }" 763 | ], 764 | [ 765 | "div.leftxx, div.leftxx { width: 0; }" 766 | ], 767 | [ 768 | "div.left > span.right+span.left { width: 0; }" 769 | ], 770 | [ 771 | ".foo .left .bar { width: 0; }" 772 | ], 773 | [ 774 | ".foo .left .bar #myid { width: 0; }" 775 | ], 776 | [ 777 | "foo-ltr[attr=x] { width: 0; }" 778 | ], 779 | [ 780 | ".a-foo.png { width: 0; }" 781 | ], 782 | [ 783 | "a.padding-left .\\31 { color: red; }" 784 | ], 785 | [ 786 | ".foo-left /* comment */ { color: red; }" 787 | ], 788 | [ 789 | "[class*=\"span\"].pull-right,.row-fluid [class*=\"span\"].pull-right { color: red; }" 790 | ], 791 | [ 792 | ".foo-left ~ .foo-right { width: 0; }" 793 | ] 794 | ] 795 | }, 796 | "leave unknown properties alone": { 797 | "cases": [ 798 | [ 799 | ".foo { xxleft: 10px; }" 800 | ], 801 | [ 802 | ".foo { xxright: 10px; }" 803 | ], 804 | [ 805 | ".foo { leftxx: 10px; }" 806 | ], 807 | [ 808 | ".foo { rightxx: 10px; }" 809 | ] 810 | ] 811 | }, 812 | "flip multiple rules": { 813 | "cases": [ 814 | [ 815 | "body { direction: ltr; float: left; } .foo { direction: ltr; float: left; }", 816 | "body { direction: rtl; float: right; } .foo { direction: rtl; float: right; }" 817 | ] 818 | ] 819 | }, 820 | "flip duplicate properties": { 821 | "cases": [ 822 | [ 823 | ".foo { float: left; float: right; float: left; }", 824 | ".foo { float: right; float: left; float: right; }" 825 | ] 826 | ] 827 | }, 828 | "do not flip rules or properties with @noflip comments": { 829 | "cases": [ 830 | [ 831 | "/* @noflip */ div { float: left; }" 832 | ], 833 | [ 834 | "/*! @noflip */ div { float: left; }" 835 | ], 836 | [ 837 | "/* @noflip */ div, .notme { float: left; }" 838 | ], 839 | [ 840 | "/* @noflip */ div { float: left; } div { float: right; }", 841 | "/* @noflip */ div { float: left; } div { float: left; }" 842 | ], 843 | [ 844 | "/* @noflip */\ndiv { float: left; }\ndiv { float: right; }", 845 | "/* @noflip */\ndiv { float: left; }\ndiv { float: left; }" 846 | ], 847 | [ 848 | "div { float: right; /* @noflip */ float: left; }", 849 | "div { float: left; /* @noflip */ float: left; }" 850 | ], 851 | [ 852 | "div\n{ float: right;\n/* @noflip */\n float: left;\n; }", 853 | "div\n{ float: left;\n/* @noflip */\n float: left;\n; }" 854 | ], 855 | [ 856 | "div\n{ float: right;\n/* @noflip */\n text-align: left\n }", 857 | "div\n{ float: left;\n/* @noflip */\n text-align: left\n }" 858 | ], 859 | [ 860 | "div\n{ /* @noflip */\ntext-align: left;\nfloat: right\n\t}", 861 | "div\n{ /* @noflip */\ntext-align: left;\nfloat: left\n\t}" 862 | ], 863 | [ 864 | "/* @noflip */div{float:left;text-align:left;}div{float:right}", 865 | "/* @noflip */div{float:left;text-align:left;}div{float:left}" 866 | ], 867 | [ 868 | "/* @noflip */\ndiv{float:left;text-align:left;}a{foo:right}", 869 | "/* @noflip */\ndiv{float:left;text-align:left;}a{foo:left}" 870 | ], 871 | [ 872 | "/* @noflip */ div.foo[bar*=baz] { left: 10px; float: left; }" 873 | ], 874 | [ 875 | "/* @noflip */ div.foo[bar^=baz] { left: 10px; float: left; }" 876 | ], 877 | [ 878 | "/* @noflip */ div.foo[bar~=baz] { left: 10px; float: left; }" 879 | ], 880 | [ 881 | "/* @noflip */ div.foo[bar=baz] { left: 10px; float: left; }" 882 | ], 883 | [ 884 | "/* @noflip */ div.foo[bar*='baz{quux'] { left: 10px; float: left; }" 885 | ] 886 | ] 887 | }, 888 | "do not flip gradient notation": { 889 | "cases": [ 890 | [ 891 | ".foo { background-image: -moz-linear-gradient(#326cc1, #234e8c); }" 892 | ], 893 | [ 894 | ".foo { background-image: -webkit-gradient(linear, 100% 0%, 0% 0%, from(#666666), to(#ffffff)); }" 895 | ] 896 | ] 897 | }, 898 | "long content": { 899 | "cases": [ 900 | [ 901 | ".x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}.x{right:0}", 902 | ".x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}.x{left:0}" 903 | ] 904 | ] 905 | }, 906 | "do not touch CSS Logical": { 907 | "options": { 908 | "transformDirInUrl": true, 909 | "transformEdgeInUrl": true 910 | }, 911 | "cases": [ 912 | [ 913 | "ul { margin-top: 0.3em; margin-right: 0; margin-bottom: 0; margin-left: 1.6em; margin-inline-start: 1.6em; margin-inline-end: 0; padding: 0; }", 914 | "ul { margin-top: 0.3em; margin-left: 0; margin-bottom: 0; margin-right: 1.6em; margin-inline-start: 1.6em; margin-inline-end: 0; padding: 0; }" 915 | ], 916 | [ 917 | "ol { margin: 0.3em 0 0 3.2em; margin-inline: 3.2em 0; padding: 0; }", 918 | "ol { margin: 0.3em 3.2em 0 0; margin-inline: 3.2em 0; padding: 0; }" 919 | ], 920 | [ 921 | ".foo { float: inline-start; text-align: end; } .bar { clear: inline-end; text-align: start; }" 922 | ], 923 | [ 924 | "div { inline-size: min-content; min-inline-size: max-content; max-inline-size: fit-content(10%); block-size: min-content; min-block-size: 10q; max-block-size: none; margin-block-start: auto; margin-block-end: 1ex; margin-inline-start: 10%; margin-inline-end: initial; margin-block: 1ex 10%; margin-inline: 20%; inset-block-start: 10px; inset-block-end: auto; inset-inline-start: auto; inset-inline-end: 10%; inset-block: 20% 10%; inset-inline: 10px; inset: 10px 20px 10px; padding-block-start: auto; bad-value-for-property; padding-block-end: 1ex; padding-inline-start: 10%; padding-inline-end: unset; padding-block: 10% 20%; padding-inline: 30%; }" 925 | ], 926 | [ 927 | "span { border-block-start-width: 1px; border-block-end-width: medium; border-inline-start-width: thin; border-inline-end-width: thick; border-block-width: thick thin; border-inline-width: medium; border-block-start-style: dotted; border-block-end-style: solid; border-inline-start-style: none; border-inline-end-style: outset; border-block-style: outset solid; border-inline-style: dotted; border-block-start-color: red; border-block-end-color: red; border-inline-start-color: red; border-inline-end-color: red; border-block-color: green; border-inline-color: transparent red; border-block-start: 1px; border-block-end: medium; border-inline-start: thin; border-inline-end: thick; border-block: thick; border-inline: red; border-start-start-radius: red; border-start-end-radius: red; border-end-start-radius: red; border-end-end-radius: red; }" 928 | ] 929 | ] 930 | }, 931 | "do not touch dir attribute selector and dir pseudo-class selector": { 932 | "options": { 933 | "transformDirInUrl": true, 934 | "transformEdgeInUrl": true 935 | }, 936 | "cases": [ 937 | [ 938 | "div[dir=rtl] { padding: 1em } html[ dir='ltr' ] { padding: 1em } div[dir=\"rtl\"] { padding: 1em }" 939 | ], 940 | [ 941 | ".x:dir( ltr ) { padding: 1em } .x:dir(rtl) :not( [ dir='ltr' ] ) { padding: 1em } .x:dir(ltr) { padding: 1em }" 942 | ], 943 | [ 944 | "span:dir(rtl) { direction: ltr; padding-right: 1em; } span[dir=ltr] { direction: rtl; padding-right: 1em; }", 945 | "span:dir(rtl) { direction: rtl; padding-left: 1em; } span[dir=ltr] { direction: ltr; padding-left: 1em; }" 946 | ] 947 | ] 948 | } 949 | } 950 | -------------------------------------------------------------------------------- /test/unit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const QUnit = require( 'qunit' ); 4 | const cssjanus = require( '../src/cssjanus' ); 5 | 6 | const testData = require( './data.json' ); 7 | 8 | for ( const name in testData ) { 9 | const data = testData[ name ]; 10 | const args = data.args || [ data.options || {} ]; 11 | 12 | QUnit.test( name, ( assert ) => { 13 | for ( let i = 0; i < data.cases.length; i++ ) { 14 | const input = data.cases[ i ][ 0 ]; 15 | const noop = data.cases[ i ][ 1 ] === undefined; 16 | const output = noop ? input : data.cases[ i ][ 1 ]; 17 | const roundtrip = data.roundtrip !== undefined ? data.roundtrip : !noop; 18 | 19 | assert.equal( 20 | cssjanus.transform( 21 | input, 22 | args[ 0 ], 23 | args[ 1 ] 24 | ), 25 | output, 26 | `case #${ i + 1 }` 27 | ); 28 | 29 | if ( roundtrip ) { 30 | // Round-trip 31 | assert.equal( 32 | cssjanus.transform( 33 | output, 34 | args[ 0 ], 35 | args[ 1 ] 36 | ), 37 | input, 38 | `case #${ i + 1 } roundtrip` 39 | ); 40 | 41 | // Keep test data clean 42 | assert.true( 43 | data.cases[ i ][ 1 ] !== input, 44 | `case #${ i + 1 } should not specify output if it matches the input` 45 | ); 46 | } 47 | } 48 | } ); 49 | } 50 | --------------------------------------------------------------------------------