From 82b24c33a70d78ef2c7a5b52604ac36c2f060dba Mon Sep 17 00:00:00 2001 From: Aleen Date: Thu, 9 May 2024 11:38:59 +0800 Subject: [PATCH] Support specifying processors during processing CSS declarations --- README.md | 57 ++ src/@types/index.ts | 6 + src/data/store.ts | 8 +- src/parsers/declarations.ts | 5 +- .../process-declaration-plugins.snapshot | 616 ++++++++++++++++++ tests/basic-options.test.ts | 19 + 6 files changed, 707 insertions(+), 4 deletions(-) create mode 100644 tests/__snapshots__/basic-options/combined/process-declaration-plugins.snapshot diff --git a/README.md b/README.md index c023c887..ea8dbbdd 100644 --- a/README.md +++ b/README.md @@ -382,6 +382,7 @@ All the options are optional, and a default value will be used if any of them is | stringMap | `PluginStringMap[]` | Check below | An array of strings maps that will be used to make the replacements of the declarations' URLs and to match the names of the rules if `processRuleNames` is `true` | | greedy | `boolean` | `false` | When greedy is `true`, the matches of `stringMap` will not take into account word boundaries | | aliases | `Record` | `{}` | A strings map to treat some declarations as others | +| processDeclarationPlugins | `Array<{name: string, priority: number, processors: PluginProcessor[]}>` | `[]` | Plugins applied when processing CSS declarations | --- @@ -1447,6 +1448,62 @@ const options = { --- +#### processDeclarationPlugins + +
Expand +

+ +Sometimes, we can register some plugins when processing CSS declarations via the `processDeclarationPlugins` options, which is helpful when we need to avoid unexpected flipping situations like `background-position`. + +##### input + +```css +.test { + background-position: 0 100%; +} +``` + +##### Convert `0` to `100%` (default) + +##### output + +```css +.test { + background-position: 100% 100%; +} +``` + +##### Set a plugin to avoid flipping + +```javascript +const options = { + processDeclarationPlugins: [ + { + name: 'avoid-flipping-background', + priority: 99, // above the core RTLCSS plugin which has a priority value of 100 + processors: [{ + expr: /(background|object)(-position(-x)?|-image)?$/i, + action: (prop, value) => ({prop, value})} + ] + } + ] +}; +``` + +##### output + +```css +.test { + background-position: 0 100%; +} +``` + +

+ +
+ +--- + Control Directives --- diff --git a/src/@types/index.ts b/src/@types/index.ts index 4bcc49e0..93a14478 100644 --- a/src/@types/index.ts +++ b/src/@types/index.ts @@ -39,6 +39,11 @@ export interface PluginStringMap { replace: strings; } +export interface PluginProcessor { + expr: RegExp; + action: (prop: string, value: string, context: object) => object; +} + export type PrefixSelectorTransformer = (prefix: string, selector: string) => string | void; export interface PluginOptions { @@ -58,6 +63,7 @@ export interface PluginOptions { stringMap?: PluginStringMap[]; greedy?: boolean; aliases?: Record; + processDeclarationPlugins?: Array<{ name: string, priority: number, processors: PluginProcessor[] }>; } export interface PluginOptionsNormalized extends Omit, 'stringMap' | 'prefixSelectorTransformer'> { diff --git a/src/data/store.ts b/src/data/store.ts index 8d23ee7f..ed7558dd 100644 --- a/src/data/store.ts +++ b/src/data/store.ts @@ -143,7 +143,8 @@ const defaultOptions = (): PluginOptionsNormalized => ({ useCalc: false, stringMap: getRTLCSSStringMap(defaultStringMap), greedy: false, - aliases: {} + aliases: {}, + processDeclarationPlugins: [] }); const store: Store = { @@ -217,7 +218,10 @@ const normalizeOptions = (options: PluginOptions): PluginOptionsNormalized => { if (options.aliases && isObjectWithStringKeys(options.aliases)) { returnOptions.aliases = options.aliases; } - return returnOptions; + return { + ...returnOptions, + processDeclarationPlugins: (options.processDeclarationPlugins || []).map(plugin => ({...plugin, directives: {control: {}, value: []}})) + }; }; const initStore = (options: PluginOptions): void => { diff --git a/src/parsers/declarations.ts b/src/parsers/declarations.ts index 5e5b9b7c..5394a65a 100644 --- a/src/parsers/declarations.ts +++ b/src/parsers/declarations.ts @@ -59,7 +59,8 @@ export const parseDeclarations = ( useCalc, stringMap, greedy, - aliases + aliases, + processDeclarationPlugins } = store.options; const deleteDeclarations: Declaration[] = []; @@ -160,7 +161,7 @@ export const parseDeclarations = ( stringMap, greedy, aliases - }); + }, processDeclarationPlugins); /* the source could be undefined in certain cases but not during the tests */ /* istanbul ignore next */ diff --git a/tests/__snapshots__/basic-options/combined/process-declaration-plugins.snapshot b/tests/__snapshots__/basic-options/combined/process-declaration-plugins.snapshot new file mode 100644 index 00000000..05794ce4 --- /dev/null +++ b/tests/__snapshots__/basic-options/combined/process-declaration-plugins.snapshot @@ -0,0 +1,616 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`[[Mode: combined]] Basic Options Tests: {processDeclarationPlugins} 1`] = ` +".test1, .test2 { + background: url("/folder/subfolder/icons/ltr/chevron-left.png"); + background-color: #FFF; + background-position: 10px 20px; + color: #666; + width: 100%; +} + +[dir="ltr"] .test1, [dir="ltr"] .test2 { + border-radius: 0 2px 0 8px; + padding-right: 20px; + text-align: left; + transform: translate(-50%, 50%); +} + +[dir="rtl"] .test1, [dir="rtl"] .test2 { + border-radius: 2px 0 8px 0; + padding-left: 20px; + text-align: right; + transform: translate(50%, 50%); +} + +.test2 { + color: red; + text-align: left; + text-align: center; +} + +/* Comment not related to rtl */ +.test3 { + margin: 1px 2px 3px; + padding: 10px 20px; + /* Property comment not related to rtl */ + text-align: center; +} + +[dir="ltr"] .test3 { + direction: ltr; +} + +[dir="rtl"] .test3 { + direction: rtl; +} + +.test4 { + font-size: 10px; + border-color: red; + transform-origin: 10px 20px; + transform: scale(0.5, 0.5); +} + +[dir="ltr"] .test4 { + border-radius: 2px 4px 8px 16px; + text-shadow: red 99px -1px 1px, blue 99px 2px 1px; +} + +[dir="rtl"] .test4 { + border-radius: 4px 2px 16px 8px; + text-shadow: red -99px -1px 1px, blue -99px 2px 1px; +} + +.test5, +.test6, +.test7 { + background: linear-gradient(0.25turn, #3F87A6, #EBF8E1, #F69D3C); + border: 1px solid 2px; + box-sizing: border-box; + position: absolute; +} + +[dir="ltr"] .test5, +[dir="ltr"] .test6, +[dir="ltr"] .test7 { + border-color: #666 #777 #888 #999; + border-width: 1px 2px 3px 4px; + left: 100px; + transform: translateX(5px); +} + +[dir="rtl"] .test5, +[dir="rtl"] .test6, +[dir="rtl"] .test7 { + border-color: #666 #999 #888 #777; + border-width: 1px 4px 3px 2px; + right: 100px; + transform: translateX(-5px); +} + +/*rtl:novalid:ignore*/ +.test8 { + background: linear-gradient(to left, #333, #333 50%, #EEE 75%, #333 75%); + display: flex; + padding-left: 10%; +} + +.test9, .test10 { + background: linear-gradient(217deg, rgba(255,0,0,.8), rgba(255,0,0,0) 70.71%), + linear-gradient(127deg, rgba(0,255,0,.8), rgba(0,255,0,0) 70.71%), + linear-gradient(336deg, rgba(0,0,255,.8), rgba(0,0,255,0) 70.71%); + padding: 0px 2px 3px 4px; +} + +[dir="ltr"] .test9, [dir="ltr"] .test10 { + left: 5px; +} + +[dir="rtl"] .test9, [dir="rtl"] .test10 { + right: 5px; +} + +[dir="ltr"] .test11:hover, +[dir="ltr"] .test11:active { + font-family: Arial, Helvetica; + font-size: 20px; + color: '#FFF'; + transform: translateY(10px); + padding: 10px; +} + +[dir="rtl"] .test11:hover, +[dir="rtl"] .test11:active { + font-family: "Droid Arabic Kufi", Arial, Helvetica; + font-size: 30px; + color: #000; + transform: translateY(10px) scaleX(-1); + padding: 10px 20px; +} + +#test12, #test13 { + left: 10px; + position: relative; + text-align: right; +} + +.test14 .test15 span { + border-left-color: #777; + margin: 10px 20px 30px 40px; + transform: translate(100%, 10%); +} + +.test16 { + padding-right: 10px; +} + +[dir="ltr"] .test16:hover { + padding-right: 20px; +} + +[dir="rtl"] .test16:hover { + padding-left: 20px; +} + +.test17 { + cursor: pointer; + padding: 10px 20px 40px 10px; + text-align: right; +} + +@media only screen and (min-device-width: 320px) { + .test17 { + cursor: wait; + } +} + +.test18 { + animation: 5s flip 1s ease-in-out, + 3s my-animation 6s ease-in-out; + font-size: 10px; +} + +[dir="ltr"] .test18 { + padding: 10px 20px 40px 10px; +} + +[dir="rtl"] .test18 { + padding: 10px 10px 40px 20px; +} + +.test18::after { + content: ''; + text-align: left; + padding-left: 5px; + margin-left: 15px; + transform: translateX(5px); +} + +[dir="ltr"] .test18::after { + left: 10px; +} + +[dir="rtl"] .test18::after { + right: 10px; +} + +@keyframes flip { + from { + transform: translateX(100px); + } + + to { + transform: translateX(0); + } +} + +@media only screen and (min-device-width: 320px) { + .test18 { + width: 100%; + } + + [dir="ltr"] .test18 { + padding: 1px 2px 3px 4px; + } + + [dir="rtl"] .test18 { + padding: 1px 4px 3px 2px; + } +} + +.test19 { + animation-delay: 1s; + animation-duration: 3s; + animation-name: my-animation; + animation-timing-function: ease-in-out; +} + +.test20 { + animation-delay: 2s; + animation-duration: 4s; + animation-name: my-animation, no-flip; + animation-timing-function: ease; +} + +@keyframes my-animation { + from { + left: 0%; + } + + to { + left: 100%; + } +} + +.test21 { + animation-delay: 1s; + animation-duration: 3s; + animation-name: no-flip; + animation-timing-function: ease-in-out; + width: 100%; +} + +@keyframes no-flip { + from { + color: #000; + } + + to { + color: #FFF; + } +} + +/* Do not add reset values in override mode */ +[dir="ltr"] .test22 { + left: 5px; + right: 10px; +} + +[dir="rtl"] .test22 { + right: 5px; + left: 10px; +} + +/* Do not create the RTL version if the result is the same */ +.test23 { + left: 10px; + right: 10px; +} + +/* Chain override */ +.test24 { + padding: 10px; +} + +[dir="ltr"] .test24 { + border: 1px solid #FFF; +} + +[dir="rtl"] .test24 { + border: 1px solid #000; +} + +[dir] .test24 { + border-bottom-color: #666; +} + +/* Automatic rename */ +.test25-ltr { + box-sizing: border-box; + color: #FFF; + font-size: 10px; + width: 100%; +} + +[dir="ltr"] .test25, [dir="ltr"] .test26-ltr, [dir="ltr"] .test27 { + background-image: url("/icons/icon-l.png") +} + +[dir="rtl"] .test25, [dir="rtl"] .test26-ltr, [dir="rtl"] .test27 { + background-image: url("/icons/icon-r.png") +} + +[dir="ltr"] .test26-rtl { + background-image: url("/icons/icon-r.png") +} + +[dir="rtl"] .test26-rtl { + background-image: url("/icons/icon-l.png") +} + +.test27-prev { + background-image: url("/icons/icon-p.png") +} + +.test27-next { + background-image: url("/icons/icon-n.png") +} + +.test28 { + font-family: 'Material Icons'; + font-weight: normal; + font-style: normal; + font-size: 24px; + display: inline-block; + line-height: 1; + text-transform: none; + letter-spacing: normal; + word-wrap: normal; + white-space: nowrap; +} + +.test28-left::before { + background-image: url("/folder/subfolder/arrow-left.png"); +} + +.test28-right::before { + background-image: url("/folder/subfolder/arrow-right.png"); +} + +.testleft29 { + border: 1px solid gray; +} + +.testleft30 { + background: url("/folder/subfolder/icon-ltr.png"); +} + +.testright30 { + background: url("/folder/subfolder/icon-rtl.png"); +} + +.test31 { + background-image: url("/icons/icon-left.png"); + border: 1px solid gray; +} + +.test32 { + align-items: center; + background-image: url("/icons/icon-left.png"); + background-repeat: no-repeat; + border: 1px solid gray; +} + +.test33 { + color: #EFEFEF; +} + +[dir="ltr"] .test33 { + left: 10px; +} + +[dir="rtl"] .test33 { + right: 10px; + height: 50px; + width: 100px; +} + +[dir="rtl"] .example34 { + color: #EFEFEF; + left: 10px; + width: 100%; +} + +[dir="rtl"] .example35 { + transform: translate(10px, 20px); +} + +.test36 { + color: #FFF; + width: 100%; +} + +[dir="ltr"] .test36 { + border-right: 1px solid #666; + padding: 10px 20px 10px 5px; + text-align: right; +} + +[dir="rtl"] .test36 { + border-left: 1px solid #666; + padding: 10px 5px 10px 20px; + text-align: left; +} + +.test37 { + color: #FFF; + width: 100%; +} + +[dir="ltr"] .test37 { + border-left: 1px solid #666; + padding: 10px 5px 10px 20px; + text-align: right; +} + +[dir="rtl"] .test37 { + border-right: 1px solid #666; + padding: 10px 20px 10px 5px; + text-align: left; +} + +.test38 { + color: #FFF; + width: 100%; +} + +[dir="ltr"] .test38 { + border-left: 1px solid #666; + padding: 10px 20px 10px 5px; + text-align: right; +} + +[dir="rtl"] .test38 { + border-right: 1px solid #666; + padding: 10px 5px 10px 20px; + text-align: left; +} + +.test39 { + width: 50%; +} + +[dir="ltr"] .test39 { + margin-right: 10px; +} + +[dir="rtl"] .test39 { + margin-left: 10px; +} + +.test40 { + color: transparent; + padding: 10px; +} + +[dir="ltr"] .test40 { + right: 5px; +} + +[dir="rtl"] .test40 { + left: 5px; +} + +.test41 { + color: #EFEFEF; +} + +[dir="ltr"] .test41 { + right: 10px; + height: 50px; + width: 100px; +} + +[dir="rtl"] .test41 { + left: 10px; +} + +[dir="ltr"] .test42 { + color: #EFEFEF; + left: 10px; + width: 100%; +} + +[dir="ltr"] .test43 { + transform: translate(10px, 20px); +} + +@keyframes normalFlip { + from { + left: 0px; + top: 0px; + } + + to { + left: 100px; + top: -100px; + } +} + +.test44 { + animation: 5s normalFlip 1s ease-in-out; +} + +@keyframes inversedFlip { + from { + left: 0px; + top: 0px; + } + + to { + left: 100px; + top: -100px; + } +} + +.test45 { + animation: 5s inversedFlip 1s ease-in-out; +} + +@media only screen and (min-device-width: 320px) { + .test46 { + cursor: wait; + } + + [dir="ltr"] .test46 { + text-align: left; + } + + [dir="rtl"] .test46 { + text-align: right; + } + + .test47 { + color: white; + } + + .test47left { + content: "\\f007"; + } + + .test47right { + content: "\\f010"; + } +} + +@media only screen and (min-device-width: 320px) { + .test48 { + cursor: wait; + } + + [dir="ltr"] .test48 { + text-align: right; + } + + [dir="rtl"] .test48 { + text-align: left; + } + + .test49 { + color: white; + } +} + +[dir="ltr"]:root { + text-align: right; +} + +[dir="rtl"]:root { + text-align: left; +} + +html .test50 { + color: red; +} + +html[dir="ltr"] .test50 { + left: 10px; +} + +html[dir="rtl"] .test50 { + right: 10px; +} + +[dir="ltr"] .test51 { + border-left: 1px solid #FFF; +} + +[dir="rtl"] .test51 { + border-right: 1px solid #FFF; +} + +[dir] .test51 { + border: none; +} + +.test52 { + color: red; + padding-block: 1px 2px; +} + +.test53 { + margin-block-start: 10px; + margin-block-end: 5px; +}" +`; diff --git a/tests/basic-options.test.ts b/tests/basic-options.test.ts index 071c285c..6f6a8332 100644 --- a/tests/basic-options.test.ts +++ b/tests/basic-options.test.ts @@ -73,6 +73,25 @@ runTests({}, (pluginOptions: PluginOptions): void => { ); expect(output.warnings()).toHaveLength(0); }); + + it('{processDeclarationPlugins}', (): void => { + const options: PluginOptions = { + ...pluginOptions, + processDeclarationPlugins: [{ + name: 'avoid-flipping-background', + priority: 99, // above the core RTLCSS plugin which has a priority value of 100 + processors: [{ + expr: /(background|object)(-position(-x)?|-image)?$/i, + action: (prop: string, value: string) => ({prop, value}) + }] + }] + }; + const output = postcss([postcssRTLCSS(options)]).process(input); + expect(output.css).toMatchSpecificSnapshot( + createSnapshotFileName(BASE_NAME,'process-declaration-plugins', pluginOptions.mode) + ); + expect(output.warnings()).toHaveLength(0); + }); });