Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>` | `{}` | A strings map to treat some declarations as others |
| processDeclarationPlugins | `DeclarationPlugin[]` | `[]` | Plugins applied when processing CSS declarations |

---

Expand Down Expand Up @@ -1447,6 +1448,62 @@ const options = {

---

#### processDeclarationPlugins

<details><summary>Expand</summary>
<p>

The intention of the processDeclarationPlugins option is to process the declarations to extend or override RTLCSS functionality. For example, we can avoid automatically flipping of `background-potion`.

##### 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%;
}
```

</p>

</details>

---

Control Directives
---

Expand Down
24 changes: 23 additions & 1 deletion src/@types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,26 @@ export interface PluginStringMap {
replace: strings;
}

export type RTLCSSPlugin = {
name: string;
priority: number;
directives: {
control: object,
value: Array<object>
};
}

export interface DeclarationPluginProcessor {
expr: RegExp;
action: (prop: string, value: string, context: object) => object;
}

export type DeclarationPlugin = {
name: string;
priority: number;
processors: DeclarationPluginProcessor[];
}

export type PrefixSelectorTransformer = (prefix: string, selector: string) => string | void;

export interface PluginOptions {
Expand All @@ -58,10 +78,12 @@ export interface PluginOptions {
stringMap?: PluginStringMap[];
greedy?: boolean;
aliases?: Record<string, string>;
processDeclarationPlugins?: DeclarationPlugin[];
}

export interface PluginOptionsNormalized extends Omit<Required<PluginOptions>, 'stringMap' | 'prefixSelectorTransformer'> {
export interface PluginOptionsNormalized extends Omit<Required<PluginOptions>, 'stringMap' | 'processDeclarationPlugins' | 'prefixSelectorTransformer'> {
stringMap: StringMap[];
plugins: RTLCSSPlugin[];
prefixSelectorTransformer: PrefixSelectorTransformer | null;
}

Expand Down
1 change: 1 addition & 0 deletions src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export const DECLARATION_TYPE = 'decl';
export const RULE_TYPE = 'rule';
export const AT_RULE_TYPE = 'atrule';
export const STRING_TYPE = 'string';
export const NUMBER_TYPE = 'number';
export const BOOLEAN_TYPE = 'boolean';
export const FUNCTION_TYPE = 'function';
export const KEYFRAMES_NAME = 'keyframes';
Expand Down
26 changes: 24 additions & 2 deletions src/data/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Rule, AtRule } from 'postcss';
import {
PluginOptions,
PluginOptionsNormalized,
DeclarationPlugin,
DeclarationPluginProcessor,
AtRulesObject,
AtRulesStringMap,
RulesObject,
Expand All @@ -16,6 +18,8 @@ import {
} from '@types';
import {
BOOLEAN_TYPE,
STRING_TYPE,
NUMBER_TYPE,
FUNCTION_TYPE,
REG_EXP_CHARACTERS_REG_EXP,
LAST_WORD_CHARACTER_REG_EXP
Expand Down Expand Up @@ -99,6 +103,18 @@ const isNotAcceptedStringMap = (stringMap: PluginStringMap[]): boolean => {
);
};

const isAcceptedProcessDeclarationPlugins = (plugins: DeclarationPlugin[]): boolean =>
Array.isArray(plugins)
&& plugins.every((plugin: DeclarationPlugin) =>
typeof plugin.name == STRING_TYPE
&& typeof plugin.priority == NUMBER_TYPE
&& Array.isArray(plugin.processors)
&& plugin.processors.every((processor: DeclarationPluginProcessor) =>
processor.expr instanceof RegExp
&& typeof processor.action === FUNCTION_TYPE
)
);

const isObjectWithStringKeys = (obj: Record<string, unknown>): boolean =>
!Object.entries(obj).some(
(entry: [string, unknown]): boolean =>
Expand Down Expand Up @@ -143,7 +159,8 @@ const defaultOptions = (): PluginOptionsNormalized => ({
useCalc: false,
stringMap: getRTLCSSStringMap(defaultStringMap),
greedy: false,
aliases: {}
aliases: {},
plugins: []
});

const store: Store = {
Expand Down Expand Up @@ -214,6 +231,11 @@ const normalizeOptions = (options: PluginOptions): PluginOptionsNormalized => {
}
});
}
if (isAcceptedProcessDeclarationPlugins(options.processDeclarationPlugins)) {
returnOptions.plugins = options.processDeclarationPlugins.map(plugin => ({
...plugin, directives: {control: {}, value: []},
}));
}
if (options.aliases && isObjectWithStringKeys(options.aliases)) {
returnOptions.aliases = options.aliases;
}
Expand Down Expand Up @@ -250,4 +272,4 @@ const initKeyframesData = (): void => {
store.keyframesRegExp = getKeyFramesRegExp(store.keyframesStringMap);
};

export { store, initStore, initKeyframesData };
export { store, initStore, initKeyframesData };
5 changes: 3 additions & 2 deletions src/parsers/declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ export const parseDeclarations = (
useCalc,
stringMap,
greedy,
aliases
aliases,
plugins
} = store.options;

const deleteDeclarations: Declaration[] = [];
Expand Down Expand Up @@ -160,7 +161,7 @@ export const parseDeclarations = (
stringMap,
greedy,
aliases
});
}, plugins);

/* the source could be undefined in certain cases but not during the tests */
/* istanbul ignore next */
Expand Down
183 changes: 183 additions & 0 deletions tests/__snapshots__/process-declaration-plugins/combined/flip.snapshot
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`[[Mode: combined]] flip background by default 1`] = `
"[dir="ltr"] .test1 {
background: url("/icons/icon-left.png") 0 100%;
}

[dir="rtl"] .test1 {
background: url("/icons/icon-left.png") 100% 100%;
}

.test2 {
background-image: url("/icons/icon-left.png");
}

[dir="ltr"] .test2 {
background-position: 0 100%;
}

[dir="rtl"] .test2 {
background-position: 100% 100%;
}

.test3 {
background-image: url("/icons/icon-left.png");
background-position-y: 100%;
}

[dir="ltr"] .test3 {
background-position-x: 0;
}

[dir="rtl"] .test3 {
background-position-x: 100%;
}

[dir="ltr"] .test4 {
object-position: 0 100%;
}

[dir="rtl"] .test4 {
object-position: 100% 100%;
}

/* inside a nested rule */
.test1 {
.test3 {
background-image: url("/icons/icon-left.png");
}

> .test4 {
background-image: url("/icons/icon-left.png");
background-position-y: 100%;
}
}

[dir="ltr"] .test1 {
&.test2 {
background: url("/icons/icon-left.png") 0 100%;
}

.test3 {
background-position: 0 100%;
}

> .test4 {
background-position-x: 0;
}

+ .test5 {
object-position: 0 100%;
}
}

[dir="rtl"] .test1 {
&.test2 {
background: url("/icons/icon-left.png") 100% 100%;
}

.test3 {
background-position: 100% 100%;
}

> .test4 {
background-position-x: 100%;
}

+ .test5 {
object-position: 100% 100%;
}
}

/* inside a keyframe animation */
@keyframes flip1 {
from {
background: url("/icons/icon-left.png") 0 100%;
}

to {
background: url("/icons/icon-left.png") 100% 100%;
}
}

@keyframes flip2 {
from {
background-image: url("/icons/icon-left.png");
background-position: 0 100%;
}

to {
background-image: url("/icons/icon-left.png");
background-position: 100% 100%;
}
}

@keyframes flip3 {
from {
background-image: url("/icons/icon-left.png");
background-position-x: 0;
background-position-y: 100%;
}

to {
background-image: url("/icons/icon-left.png");
background-position-x: 100%;
background-position-y: 100%;
}
}

@keyframes flip4 {
from {
object-position: 0 100%;
}

to {
object-position: 100% 100%;
}
}

/* inside a media-query */
@media screen and (max-width: 800px) {
[dir="ltr"] .test1 {
background: url("/icons/icon-left.png") 0 100%;
}

[dir="rtl"] .test1 {
background: url("/icons/icon-left.png") 100% 100%;
}

.test2 {
background-image: url("/icons/icon-left.png");
}

[dir="ltr"] .test2 {
background-position: 0 100%;
}

[dir="rtl"] .test2 {
background-position: 100% 100%;
}

.test3 {
background-image: url("/icons/icon-left.png");
background-position-y: 100%;
}

[dir="ltr"] .test3 {
background-position-x: 0;
}

[dir="rtl"] .test3 {
background-position-x: 100%;
}

[dir="ltr"] .test4 {
object-position: 0 100%;
}

[dir="rtl"] .test4 {
object-position: 100% 100%;
}
}"
`;
Loading