├── .gitignore
├── .vscode
└── settings.json
├── CODE_OF_CONDUCT.md
├── Cargo.toml
├── LICENSE
├── README.md
├── examples
├── README.md
├── style
│ ├── Cargo.toml
│ ├── Trunk.toml
│ ├── build.rs
│ ├── index.html
│ └── src
│ │ ├── lib.rs
│ │ └── main.rs
├── style_modularized
│ ├── Cargo.toml
│ ├── Trunk.toml
│ ├── build.rs
│ ├── index.html
│ └── src
│ │ ├── lib.rs
│ │ └── main.rs
├── style_parent_child
│ ├── Cargo.toml
│ ├── Trunk.toml
│ ├── build.rs
│ ├── index.html
│ └── src
│ │ ├── lib.rs
│ │ └── main.rs
├── style_sheet
│ ├── Cargo.toml
│ ├── Trunk.toml
│ ├── build.rs
│ ├── index.html
│ └── src
│ │ ├── hello.css
│ │ ├── lib.rs
│ │ └── main.rs
├── style_sheet_str
│ ├── Cargo.toml
│ ├── index.html
│ └── src
│ │ ├── button.css
│ │ ├── lib.rs
│ │ └── main.rs
└── style_str
│ ├── Cargo.toml
│ ├── index.html
│ └── src
│ ├── lib.rs
│ └── main.rs
├── rust-toolchain.toml
├── stylers
├── Cargo.toml
└── src
│ └── lib.rs
├── stylers_core
├── Cargo.toml
└── src
│ ├── lib.rs
│ ├── style
│ ├── css_at_rule.rs
│ ├── css_style_declar.rs
│ ├── css_style_rule.rs
│ ├── css_style_sheet.rs
│ ├── mod.rs
│ └── utils.rs
│ └── style_sheet
│ ├── css_at_rule.rs
│ ├── css_style_declar.rs
│ ├── css_style_rule.rs
│ ├── css_style_sheet.rs
│ └── mod.rs
└── stylers_macro
├── Cargo.toml
├── src
└── lib.rs
└── tests
├── samples
├── at_rules.css
├── basics.css
├── custom_pseudo.css
├── pseudo.css
├── relations.css
└── special_at_rules.css
├── style.rs
└── style_sheet.rs
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
3 | target/
4 | dist/
5 | .vscode/
6 |
7 |
8 |
9 | **/Cargo.lock
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "rust-analyzer.linkedProjects": [
3 | "./stylers/Cargo.toml"
4 | ]
5 | }
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | _This Code of Conduct is based on the [Rust Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct)
4 | and the [Bevy Code of Conduct](https://raw.githubusercontent.com/bevyengine/bevy/main/CODE_OF_CONDUCT.md),
5 | which are adapted from the [Node.js Policy on Trolling](http://blog.izs.me/post/30036893703/policy-on-trolling)
6 | and the [Contributor Covenant](https://www.contributor-covenant.org)._
7 |
8 | ## Our Pledge
9 |
10 | We as members, contributors, and leaders pledge to make participation in our
11 | community a harassment-free experience for everyone, regardless of age, body
12 | size, visible or invisible disability, ethnicity, sex characteristics, gender
13 | identity and expression, level of experience, education, socio-economic status,
14 | nationality, personal appearance, race, religion, or sexual identity
15 | and orientation.
16 |
17 | We pledge to act and interact in ways that contribute to an open, welcoming,
18 | diverse, inclusive, and healthy community.
19 |
20 | ## Our Standards
21 |
22 | We are a community of people learning and exploring how to build better web applications
23 | with Rust. When interacting with one another, please remember that there are no experts and there are
24 | no stupid questions. Assume the best in other people's communication, and take a step back if
25 | you find yourself getting defensive.
26 |
27 | Please note the following guidelines as well:
28 |
29 | * Please avoid using overtly sexual aliases or other nicknames that might detract from a friendly, safe and welcoming environment for all.
30 | * Please be kind and courteous. There’s no need to be mean or rude.
31 | * Respect that people have differences of opinion and that every design or implementation choice carries a trade-off and numerous costs. There is seldom a right answer.
32 | * Please keep unstructured critique to a minimum. If you have solid ideas you want to experiment with, make a fork and see how it works.
33 | * We will exclude you from interaction if you insult, demean or harass anyone. That is not welcome behavior. We interpret the term “harassment” as including the definition in the [Citizen Code of Conduct](https://github.com/stumpsyn/policies/blob/master/citizen_code_of_conduct.md); if you have any lack of clarity about what might be included in that concept, please read their definition. In particular, we don’t tolerate behavior that excludes people in socially marginalized groups.
34 | * Private harassment is also unacceptable. No matter who you are, if you feel you have been or are being harassed or made uncomfortable by a community member, please contact the maintainers immediately. Whether you’re a regular contributor or a newcomer, we care about making this community a safe place for you and we’ve got your back.
35 | * Do not make casual mention of slavery or indentured servitude and/or false comparisons of one's occupation or situation to slavery. Please consider using or asking about alternate terminology when referring to such metaphors in technology.
36 | * Likewise any spamming, trolling, flaming, baiting or other attention-stealing behavior is not welcome.
37 |
38 | ## Moderation
39 |
40 | These are the policies for upholding [our community’s standards of conduct](#our-standards). If you feel that a thread needs moderation, please contact the maintainers.
41 |
42 | 1. Remarks that violate the community standards of conduct, including hateful, hurtful, oppressive, or exclusionary remarks, are not allowed. (Cursing is allowed, but never targeting another user, and never in a hateful manner).
43 | 2. Remarks that maintainers find inappropriate, whether listed in the code of conduct or not, are also not allowed.
44 | 3. Maintainers will first respond to such remarks with a warning.
45 | 4. If the warning is unheeded, the user will be “kicked,” i.e., kicked out of the communication channel to cool off.
46 | 5. If the user comes back and continues to make trouble, they will be banned, i.e., indefinitely excluded.
47 | 6. Maintainers may choose at their discretion to un-ban the user if it was a first offense and they offer the offended party a genuine apology.
48 | 7. If a maintainer bans someone and you think it was unjustified, please take it up with that maintainer, or with a different maintainer, in private. Complaints about bans in-channel are not allowed.
49 | 8. Maintainers are held to a higher standard than other community members. If a maintainer creates an inappropriate situation, they should expect less leeway than others.
50 |
51 | The enforcement policies in the code of conduct apply to all official venues, including Discord channels, GitHub repositories, and all other forums.
52 | Footer
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | members = ["stylers_core", "stylers", "stylers_macro"]
3 | exclude = ["examples"]
4 | resolver = "2"
5 |
6 | [workspace.dependencies]
7 | stylers = { path = "./stylers" }
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Abishek P
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Stylers
2 | - Scoped CSS for Rust web frameworks like Leptos.
3 | - `style!` macro is for writing css inside rust functions directly. It will validates css properties as well.
4 | - `style_sheet!` macro is for writing css in external css file and importing that inside rust functions.
5 | - `style_str!` macro is same as `style!` macro but returns the tuple `(class_name, style_val)` instead of saving the style_val to the separate file.
6 | - `style_sheet_str!` this same as `style_sheet!` macro but returns the tuple `(class_name, style_val)` instead of saving the style_val to the separate file.
7 |
8 | ## Important Note
9 | - This Readme file is for the latest relase of stylers 1.0.0-alpha. You can find the readme for previous versions [here](https://crates.io/crates/stylers/0.3.2)
10 |
11 | ## Installtion
12 | ```cargo add stylers```
13 |
14 | ## Prerequisite
15 | - If you are using `style` or `style_sheet` macro, then you have to add the `stylers` crate as both dependencies and build-dependencies in your Cargo.toml file.
16 | ```
17 | [dependencies]
18 | stylers = { version = "*" }
19 |
20 | [build-dependencies]
21 | stylers = { version = "*" }
22 | ```
23 |
24 | - Then you have to add `build.rs` file in your root directory and add the below code snippet in it.
25 | ```rust
26 | use stylers::build;
27 |
28 | fn main() {
29 | build(Some(String::from("./target/main.css")));
30 | }
31 | ```
32 | - In the above case output css file will be generated in the `./target/main.css` path. You can include that `main.css` file in your `index.html` file.(**Or If you are using a build like Trunk.rs you have to follow appropriate methods to include the main.css file to your project**).
33 |
34 | You can find the importance of these new changes [here](https://github.com/abishekatp/stylers/issues/35).
35 |
36 | ## Leptos Example
37 | **Note :Leptos version > 0.4.9 has some new changes. But stylers works the same way in all versions of leptos**
38 |
39 | #### style!
40 | ```rust
41 | #[component]
42 | fn Hello(cx: Scope, name: &'static str) -> impl IntoView {
43 | let styler_class = style! {
44 | #two{
45 | color: blue;
46 | }
47 | div.one{
48 | color: red;
49 | content: raw_str(r#"\hello"#);
50 | font: "1.3em/1.2" Arial, Helvetica, sans-serif;
51 | }
52 | div {
53 | border: 1px solid black;
54 | margin: 25px 50px 75px 100px;
55 | background-color: lightblue;
56 | }
57 | h2 {
58 | color: purple;
59 | }
60 | @media only screen and (max-width: 1000px) {
61 | h3 {
62 | background-color: lightblue;
63 | color: blue
64 | }
65 | }
66 | };
67 |
68 | view! {class = styler_class,
69 |
70 |
"Hello"
71 | "World"
72 | {name}
73 | "Hello Kanna"
74 |
75 | }
76 | }
77 | ```
78 | #### style_sheet!
79 | ```rust
80 | #[component]
81 | fn Hello(cx: Scope, name: &'static str) -> impl IntoView {
82 | let class_name = style_sheet!("./hello.css");
83 | view! {class = class_name,
84 |
85 |
"Hello"
86 | "World"
87 |
88 | }
89 | }
90 | ```
91 | - In the above case ```hello.css``` file is inside the `root` directory of the project.
92 |
93 | #### style_str!
94 | ```rust
95 | #[component]
96 | pub fn GreenButton(cx: Scope) -> impl IntoView {
97 | let (class_name, style_val) = style_str! {
98 | button {
99 | background-color: green;
100 | border-radius: 8px;
101 | border-style: none;
102 | box-sizing: border-box;
103 | color: yellow;
104 | cursor: pointer;
105 | display: inline-block;
106 | font-family: r#"Haas Grot Text R Web"#, r#"Helvetica Neue"#, Helvetica, Arial, sans-serif;
107 | font-size: 14px;
108 | font-weight: 500;
109 | height: 40px;
110 | line-height: 20px;
111 | list-style: none;
112 | margin: 0;
113 | outline: none;
114 | padding: 10px 16px;
115 | position: relative;
116 | text-align: center;
117 | text-decoration: none;
118 | transition: color 100ms;
119 | vertical-align: baseline;
120 | user-select: none;
121 | -webkit-user-select: none;
122 | }
123 | button:hover{
124 | background-color: yellow;
125 | color: green;
126 | }
127 | };
128 |
129 | view! {class = class_name,
130 |
131 | "I am green button"
132 | }
133 | }
134 | ```
135 |
136 | #### style_sheet_str!
137 | ```rust
138 | #[component]
139 | pub fn BlueButton() -> impl IntoView {
140 | let (class_name, style_val) = style_sheet_str!("./src/button.css");
141 |
142 | view! {class = class_name,
143 |
144 | "I am blue button"
145 | }
146 | }
147 | ```
148 | - In this case ```button.css``` file is inside the `src` directory of the project.
149 |
150 | ## Custom pseudo classes
151 | - In some situations we may need our css to affect `deep down` the dom tree. To achieve this we have custom pseudo class called `:deep()`. For example below css is valid one.
152 | #### Input
153 | ```css
154 | div :deep(h3) {
155 | color: orange;
156 | }
157 | ```
158 | #### Output
159 | ```css
160 | div.l-243433 h3{color: orange;}
161 | ```
162 |
163 | - If you want your particular css to be `global` you can use `:deep()` directive without any prceding selectors it.
164 | #### Input
165 | ```css
166 | :deep(h3 div) {
167 | color: orange;
168 | }
169 | ```
170 | #### Ouput
171 | ```css
172 | h3 div{color: orange;}
173 | ```
174 |
175 | ## How it works:
176 | - This `stylers::build` method will parse all the rust files in the path `/src/**/*.rs` during build step to find the places the `style` and `style_sheet` macros has been used and generate single output css file.
177 | - `style_str` and `style_sheet_str` directly returns the tuple (class_name, output_css).
178 |
179 |
180 | ## Edge cases handled for `style!` macros
181 | - By default double quotes ( " ) around css property values will be removed. If user wants to retain the double quotes they have to wrap it using ```raw_str``` as given below:
182 | - these rules apply for both `style!` and `style_str!` macros
183 | #### Input
184 | ```rust
185 | style!(
186 | div{
187 | content: raw_str(r#"\hello"#);
188 | font: "1.3em/1.2" Arial;
189 | }
190 | )
191 | ```
192 | #### Output
193 | ```css
194 | div.l-23432{
195 | content: "\hello";
196 | font: 1.3em/1.2 Arial;
197 | }
198 | ```
199 |
200 | ## Optional build process using Trunk(Only when you use `style!` or `style_sheet!` macro )
201 | - You have to include generated main.css in the index.html
202 | (e.g ``` ```).
203 |
204 | - In ```Trunk.toml``` you have to add the below lines to move the the `main.css` file from `./target/` directory to `./dist/` directory.
205 | ```toml
206 | [[hooks]]
207 | stage = "post_build"
208 | command = "sh"
209 | command_arguments = ["-c", "cp ./target/main.css $TRUNK_STAGING_DIR/"]
210 | ```
211 | - when you are including external css file using `style_sheet!` macro, whenever you make some changes in your css file you have to save corresponding rust file(*.rs) for css to be updated on the browser. For more info about trunk refer [here](https://trunkrs.dev/commands/).
212 | - if something is odd with styling, delete the `./target/stylers` directory and rebuild your package. If the problem persists please raise an issue here.
213 |
214 |
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | # Examples
2 |
3 | To run examples you need `trunk` installed. You can install it with `cargo install trunk`.
4 |
5 | Once `trunk` is installed, you can run the examples by going their respective folders and running:
6 |
7 | ```sh
8 | trunk serve --open
9 | ```
10 |
--------------------------------------------------------------------------------
/examples/style/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "style_macro"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | leptos = { version = "0.5.2", features = ["csr"] }
10 | gloo = "0.10.0"
11 | stylers = { path = "../../stylers" }
12 |
13 | [build-dependencies]
14 | stylers = { path = "../../stylers" }
15 |
--------------------------------------------------------------------------------
/examples/style/Trunk.toml:
--------------------------------------------------------------------------------
1 | [[hooks]]
2 | stage = "post_build"
3 | command = "sh"
4 | command_arguments = ["-c", "cp ./target/main.css $TRUNK_STAGING_DIR/"]
5 |
--------------------------------------------------------------------------------
/examples/style/build.rs:
--------------------------------------------------------------------------------
1 | use stylers::build;
2 |
3 | fn main() {
4 | build(Some(String::from("./target/main.css")));
5 | }
6 |
--------------------------------------------------------------------------------
/examples/style/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Stylers
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/examples/style/src/lib.rs:
--------------------------------------------------------------------------------
1 | use leptos::*;
2 | use stylers::style;
3 |
4 | #[component]
5 | fn Hello(name: &'static str) -> impl IntoView {
6 | //note: we will trim all double quotes by default unless it is wrapped with raw_str()
7 | let class_name = style! {
8 | // this comment will be ignored
9 | div {
10 | border: 1px solid black; /* This comment also will be ignored */
11 | margin: 25px 50px 75px 100px;
12 | background-color: lightblue;
13 | // The macro trims all double quotes by default unless it is wrapped with raw_str()
14 | content: raw_str(r#"\hello"#);
15 | font: "1.3em/1.2" Arial, Helvetica, sans-serif;
16 | }
17 | .two{
18 | color: orange;
19 | }
20 | div .one p{
21 | color: blue;
22 | }
23 | div.one{
24 | color: red;
25 | }
26 | div #two{
27 | color: blue;
28 | }
29 | h2,a {
30 | color: red;
31 | }
32 | .one:hover{
33 | background-color: yellow;
34 | }
35 | p:lang(it){
36 | background: yellow;
37 | }
38 | p::before {
39 | content: raw_str("Read this: ");
40 | }
41 | .example-url{
42 | content: r#"url("https://picsum.photos/200/300")"#;
43 | }
44 | };
45 |
46 | view! {class = class_name,
47 |
48 |
"Hello"
49 |
"World"
50 |
{name}
51 |
"Hello Kanna"
52 |
"This is example content"
53 |
"Visit the link"
54 |
55 |
56 | }
57 | }
58 |
59 | #[component]
60 | pub fn Abi() -> impl IntoView {
61 | let class_name = style! {
62 | // we can use this :deep() pseudo class in external css file as well.
63 | div :deep(h3){
64 | color: orange;
65 | }
66 | @media only screen and (max-width: 1000px) {
67 | h3 {
68 | background-color: lightblue;
69 | color: blue
70 | }
71 | }
72 | :deep(.rollUp ) .deep-text{
73 | color: orange;
74 | font-size: 1.5rem;
75 | }
76 | };
77 | view! {class = class_name,
78 |
79 |
80 |
"Hai"
81 |
82 | :deep directive test
83 |
84 |
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/examples/style/src/main.rs:
--------------------------------------------------------------------------------
1 | use gloo::console;
2 | use leptos::*;
3 | use style_macro::*;
4 |
5 | pub fn main() {
6 | console::log!("Hello, stylers!");
7 | mount_to_body(|| view! { });
8 | }
9 |
--------------------------------------------------------------------------------
/examples/style_modularized/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "style_modularized"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | leptos = { version = "0.5.2", features = ["csr"] }
10 | stylers = { path = "../../stylers" }
11 |
12 | [build-dependencies]
13 | stylers = { path = "../../stylers" }
14 |
--------------------------------------------------------------------------------
/examples/style_modularized/Trunk.toml:
--------------------------------------------------------------------------------
1 | [[hooks]]
2 | stage = "post_build"
3 | command = "sh"
4 | command_arguments = ["-c", "cp ./target/main.css $TRUNK_STAGING_DIR/"]
5 |
--------------------------------------------------------------------------------
/examples/style_modularized/build.rs:
--------------------------------------------------------------------------------
1 | use stylers::build;
2 |
3 | fn main() {
4 | build(Some(String::from("./target/main.css")));
5 | }
6 |
--------------------------------------------------------------------------------
/examples/style_modularized/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Stylers
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/examples/style_modularized/src/lib.rs:
--------------------------------------------------------------------------------
1 | use leptos::*;
2 | use stylers::{style, style_str};
3 |
4 | #[component]
5 | pub fn GreenButton() -> impl IntoView {
6 | let (local_class, style_val) = style_str! {
7 | button{
8 | background-color: green;
9 | }
10 | button:hover{
11 | background-color: yellow;
12 | color: green;
13 | }
14 | };
15 | let common_class = button_style();
16 | let class_name = format!("{} {}", common_class, local_class);
17 |
18 | view! {class = {class_name.clone()},
19 |
20 | "I am green button"
21 | }
22 | }
23 |
24 | #[component]
25 | pub fn BlueButton() -> impl IntoView {
26 | let (local_class, style_val) = style_str! {"BlueButton",
27 | button{
28 | background-color: blue;
29 | }
30 | button:hover{
31 | background-color: yellow;
32 | color: blue;
33 | }
34 | };
35 | let common_class = button_style();
36 | let class_name = format!("{} {}", common_class, local_class);
37 |
38 | view! {class = {class_name.clone()},
39 |
40 | "I am blue button"
41 | }
42 | }
43 |
44 | pub fn button_style() -> String {
45 | //note: we can even use style_str and get the style string wherever we use this style
46 | // but that will populate same style in multiple places at the DOM.
47 | let class = style! {
48 | button {
49 | background-color: #EA4C89;
50 | border-radius: 8px;
51 | border-style: none;
52 | box-sizing: border-box;
53 | color: yellow;
54 | cursor: pointer;
55 | display: inline-block;
56 | font-family: r#"Haas Grot Text R Web"#, r#"Helvetica Neue"#, Helvetica, Arial, sans-serif;
57 | font-size: 14px;
58 | font-weight: 500;
59 | height: 40px;
60 | line-height: 20px;
61 | list-style: none;
62 | margin: 0;
63 | outline: none;
64 | padding: 10px 16px;
65 | position: relative;
66 | text-align: center;
67 | text-decoration: none;
68 | transition: color 100ms;
69 | vertical-align: baseline;
70 | user-select: none;
71 | -webkit-user-select: none;
72 | }
73 | button:hover{
74 | background-color: yellow;
75 | }
76 | };
77 |
78 | class.to_string()
79 | }
80 |
--------------------------------------------------------------------------------
/examples/style_modularized/src/main.rs:
--------------------------------------------------------------------------------
1 | use leptos::*;
2 | use style_modularized::*;
3 |
4 | fn main() {
5 | mount_to_body(|| view! {
});
6 | }
7 |
--------------------------------------------------------------------------------
/examples/style_parent_child/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "style_parent_child"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | leptos = { version = "0.5.2", features = ["csr"] }
10 | stylers = { path = "../../stylers" }
11 |
12 | [build-dependencies]
13 | stylers = { path = "../../stylers" }
14 |
--------------------------------------------------------------------------------
/examples/style_parent_child/Trunk.toml:
--------------------------------------------------------------------------------
1 | [[hooks]]
2 | stage = "post_build"
3 | command = "sh"
4 | command_arguments = ["-c", "cp ./target/main.css $TRUNK_STAGING_DIR/"]
5 |
--------------------------------------------------------------------------------
/examples/style_parent_child/build.rs:
--------------------------------------------------------------------------------
1 | use stylers::build;
2 |
3 | fn main() {
4 | build(Some(String::from("./target/main.css")));
5 | }
6 |
--------------------------------------------------------------------------------
/examples/style_parent_child/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Stylers
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/examples/style_parent_child/src/lib.rs:
--------------------------------------------------------------------------------
1 | use leptos::*;
2 | use stylers::{style, style_str};
3 |
4 | #[component]
5 | pub fn Parent() -> impl IntoView {
6 | let class_name = style! {
7 | button {
8 | background-color: green;
9 | border-radius: 8px;
10 | border-style: none;
11 | box-sizing: border-box;
12 | color: yellow;
13 | cursor: pointer;
14 | display: inline-block;
15 | font-family: r#"Haas Grot Text R Web"#, r#"Helvetica Neue"#, Helvetica, Arial, sans-serif;
16 | font-size: 14px;
17 | font-weight: 500;
18 | height: 40px;
19 | line-height: 20px;
20 | list-style: none;
21 | margin: 0;
22 | outline: none;
23 | padding: 10px 16px;
24 | position: relative;
25 | text-align: center;
26 | text-decoration: none;
27 | transition: color 100ms;
28 | vertical-align: baseline;
29 | user-select: none;
30 | -webkit-user-select: none;
31 | }
32 | button:hover{
33 | background-color: yellow;
34 | color: green;
35 | }
36 | };
37 |
38 | view! {class = {class_name},
39 | "I am green button"
40 |
41 | }
42 | }
43 |
44 | #[component]
45 | fn Child(class_name: String) -> impl IntoView {
46 | let (local_class, style_val) = style_str! {
47 | button{
48 | background-color: blue;
49 | }
50 | button:hover{
51 | background-color: yellow;
52 | color: blue;
53 | }
54 | };
55 | let class_name = format!("{} {}", class_name, local_class);
56 | view! {class = {class_name.clone()},
57 |
58 | "I am blue button"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/examples/style_parent_child/src/main.rs:
--------------------------------------------------------------------------------
1 | use leptos::*;
2 | use style_parent_child::*;
3 |
4 | fn main() {
5 | mount_to_body(|| view! { });
6 | }
7 |
--------------------------------------------------------------------------------
/examples/style_sheet/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "style_sheet_macro"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | leptos = { version = "0.5.2", features = ["csr"] }
10 | stylers = { path = "../../stylers" }
11 |
12 | [build-dependencies]
13 | stylers = { path = "../../stylers" }
14 |
--------------------------------------------------------------------------------
/examples/style_sheet/Trunk.toml:
--------------------------------------------------------------------------------
1 | [[hooks]]
2 | stage = "post_build"
3 | command = "sh"
4 | command_arguments = ["-c", "cp ./target/main.css $TRUNK_STAGING_DIR/"]
5 |
--------------------------------------------------------------------------------
/examples/style_sheet/build.rs:
--------------------------------------------------------------------------------
1 | use stylers::build;
2 |
3 | fn main() {
4 | build(Some(String::from("./target/main.css")));
5 | }
6 |
--------------------------------------------------------------------------------
/examples/style_sheet/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Stylers
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/examples/style_sheet/src/hello.css:
--------------------------------------------------------------------------------
1 | /* comment is added here*/
2 | div {
3 | border: 1px solid black;
4 | margin: 25px 50px 75px 100px;
5 | /* comment is added here*/
6 | background-color: lightblue;
7 | content: "hello";
8 | font: 1.3em/1.2 Arial, Helvetica, sans-serif;
9 | }
10 |
11 | .two {
12 | color: yellow;
13 | }
14 |
15 | div .one p {
16 | color: blue;
17 | }
18 |
19 | div.one {
20 | color: orange;
21 | }
22 |
23 | div #two {
24 | color: blue;
25 | }
26 |
27 | h2,
28 | a {
29 | color: purple;
30 | }
31 |
32 | .one:hover {
33 | background-color: green;
34 | }
35 |
36 | p:lang(it) {
37 | background: yellow;
38 | }
39 |
40 | p::before {
41 | content: "Read this: ";
42 | }
--------------------------------------------------------------------------------
/examples/style_sheet/src/lib.rs:
--------------------------------------------------------------------------------
1 | use leptos::*;
2 | use stylers::{style, style_sheet};
3 |
4 | #[component]
5 | fn Hello(name: &'static str) -> impl IntoView {
6 | let class_name = style_sheet!("./src/hello.css");
7 |
8 | view! {class = class_name,
9 |
10 |
"Hello"
11 |
"World"
12 |
{name}
13 |
"Hello Kanna"
14 |
"This is example conent"
15 |
"Visit the link"
16 |
17 | }
18 | }
19 |
20 | #[component]
21 | pub fn Abi() -> impl IntoView {
22 | let class_name = style! {
23 | h3{
24 | background-color: blue;
25 | }
26 | @media only screen and (max-width: 1000px) {
27 | h3 {
28 | background-color: lightblue;
29 | color: blue
30 | }
31 | }
32 | };
33 | view! {class = class_name,
34 |
35 | "Hai"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/examples/style_sheet/src/main.rs:
--------------------------------------------------------------------------------
1 | use leptos::*;
2 | use style_sheet_macro::*;
3 |
4 | pub fn main() {
5 | println!["Hello, stylers!"];
6 | mount_to_body(|| view! { });
7 | }
8 |
--------------------------------------------------------------------------------
/examples/style_sheet_str/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "style_sheet_str"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | leptos = { version = "0.5.2", features = ["csr"] }
10 | stylers = { path = "../../stylers" }
11 |
--------------------------------------------------------------------------------
/examples/style_sheet_str/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Stylers
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/examples/style_sheet_str/src/button.css:
--------------------------------------------------------------------------------
1 | button {
2 | background-color: blue;
3 | border-radius: 8px;
4 | border-style: none;
5 | box-sizing: border-box;
6 | color: yellow;
7 | cursor: pointer;
8 | display: inline-block;
9 | font-family: "Haas Grot Text R Web", "Helvetica Neue", Helvetica, Arial, sans-serif;
10 | font-size: 14px;
11 | font-weight: 500;
12 | height: 40px;
13 | line-height: 20px;
14 | list-style: none;
15 | margin: 0;
16 | outline: none;
17 | padding: 10px 16px;
18 | position: relative;
19 | text-align: center;
20 | text-decoration: none;
21 | transition: color 100ms;
22 | vertical-align: baseline;
23 | user-select: none;
24 | -webkit-user-select: none;
25 | }
26 |
27 | button:hover {
28 | background-color: yellow;
29 | color: blue;
30 | }
--------------------------------------------------------------------------------
/examples/style_sheet_str/src/lib.rs:
--------------------------------------------------------------------------------
1 | use leptos::*;
2 | use stylers::style_sheet_str;
3 |
4 | #[component]
5 | pub fn BlueButton() -> impl IntoView {
6 | let (class_name, style_val) = style_sheet_str!("./src/button.css");
7 |
8 | view! {class = class_name,
9 |
10 | "I am blue button"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/examples/style_sheet_str/src/main.rs:
--------------------------------------------------------------------------------
1 | use leptos::*;
2 | use style_sheet_str::*;
3 |
4 | fn main() {
5 | mount_to_body(|| view! { });
6 | }
7 |
--------------------------------------------------------------------------------
/examples/style_str/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "style_str"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | leptos = { version = "0.5.2", features = ["csr"] }
10 | stylers = { path = "../../stylers" }
11 |
--------------------------------------------------------------------------------
/examples/style_str/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Stylers
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/examples/style_str/src/lib.rs:
--------------------------------------------------------------------------------
1 | use leptos::*;
2 | use stylers::style_str;
3 |
4 | #[component]
5 | pub fn GreenButton() -> impl IntoView {
6 | let (class_name, style_val) = style_str! {
7 | button {
8 | background-color: green;
9 | border-radius: 8px;
10 | border-style: none;
11 | box-sizing: border-box;
12 | color: yellow;
13 | cursor: pointer;
14 | display: inline-block;
15 | font-family: r#"Haas Grot Text R Web"#, r#"Helvetica Neue"#, Helvetica, Arial, sans-serif;
16 | font-size: 14px;
17 | font-weight: 500;
18 | height: 40px;
19 | line-height: 20px;
20 | list-style: none;
21 | margin: 0;
22 | outline: none;
23 | padding: 10px 16px;
24 | position: relative;
25 | text-align: center;
26 | text-decoration: none;
27 | transition: color 100ms;
28 | vertical-align: baseline;
29 | user-select: none;
30 | -webkit-user-select: none;
31 | }
32 | button:hover{
33 | background-color: yellow;
34 | color: green;
35 | }
36 | .one .two{
37 | font-size: 2.5rem;
38 | font-weight: 900;
39 | }
40 | };
41 |
42 | view! {class = class_name,
43 |
44 | "I am green button"
45 |
46 | "This is Large Text"
47 |
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/examples/style_str/src/main.rs:
--------------------------------------------------------------------------------
1 | use leptos::*;
2 | use style_str::*;
3 |
4 | fn main() {
5 | mount_to_body(|| view! { });
6 | }
7 |
--------------------------------------------------------------------------------
/rust-toolchain.toml:
--------------------------------------------------------------------------------
1 | [toolchain]
2 | channel = "nightly-2023-07-28"
3 | profile = "default"
4 |
--------------------------------------------------------------------------------
/stylers/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "stylers"
3 | version = "1.0.0-alpha"
4 | edition = "2021"
5 | authors = ["Abishek P"]
6 | license = "MIT"
7 | repository = "https://github.com/abishekatp/stylers"
8 | description = "Scoped CSS for Rust web frameworks like leptos"
9 | keywords = ["web", "css", "leptos"]
10 | categories = ["web-programming"]
11 | readme = "../README.md"
12 |
13 | [dependencies]
14 | glob = "0.3.1"
15 | syn = { version = "2.0.15", features = ["full"] }
16 | stylers_core = "1.0.2"
17 | stylers_macro = "1.0.2"
18 | proc-macro2 = "1.0.60"
19 |
--------------------------------------------------------------------------------
/stylers/src/lib.rs:
--------------------------------------------------------------------------------
1 | use glob::glob;
2 |
3 | use std::fs::File;
4 | use std::io::{self, Write};
5 | use std::{borrow::Borrow, env::current_dir, fs};
6 | use stylers_core::Class;
7 | use stylers_core::{from_str, from_ts};
8 | use syn::{Expr, Item, Stmt};
9 |
10 | pub use stylers_macro::style;
11 | pub use stylers_macro::style_sheet;
12 | pub use stylers_macro::style_sheet_str;
13 | pub use stylers_macro::style_str;
14 |
15 | macro_rules! p {
16 | ($($tokens: tt)*) => {
17 | println!("cargo:warning={}", format!($($tokens)*))
18 | }
19 | }
20 |
21 | pub fn build(output_path: Option) {
22 | let pattern = format!("{}/src/**/*.rs", current_dir().unwrap().to_str().unwrap());
23 | let mut output_css = String::from("");
24 | p!(
25 | "{}",
26 | "===============================Stylers debug output start==============================="
27 | );
28 | for file in glob(&pattern).unwrap() {
29 | let file = file.unwrap();
30 | let content = fs::read_to_string(file).expect("Failed to read file");
31 | let ast = syn::parse_file(&content).unwrap();
32 |
33 | // check the each item in the *.rs file
34 | for item in ast.items {
35 | // check if the item is of type Function.
36 | if let Item::Fn(fn_def) = item {
37 | let _componet_name = &fn_def.sig.ident;
38 | // check each statement in the function
39 | for stmt in fn_def.block.stmts {
40 | // check if any of the statment is of the form `let any_valid_variable = style!{}`
41 | if let Stmt::Local(let_bin) = stmt {
42 | if let Some(init) = let_bin.init {
43 | if let Expr::Macro(expr_mac) = init.expr.borrow() {
44 | if let Some(path_seg) = expr_mac.mac.path.segments.last() {
45 | let macro_name = path_seg.ident.clone().to_string();
46 | // p!("macro_name:{:?}", macro_name);
47 |
48 | if macro_name == *"style" {
49 | let ts = expr_mac.mac.tokens.clone();
50 | let class = Class::rand_class_from_seed(ts.to_string());
51 | let token_stream = ts.into_iter();
52 | let (scoped_css, _) = from_ts(token_stream, &class, false);
53 | output_css += &scoped_css;
54 | }
55 |
56 | if macro_name == *"style_sheet" {
57 | let ts = expr_mac.mac.tokens.clone();
58 | let file_path = ts.to_string();
59 | let file_path = file_path.trim_matches('"');
60 | let css_content = std::fs::read_to_string(file_path)
61 | .expect("Expected to read file");
62 |
63 | let class =
64 | Class::rand_class_from_seed(css_content.to_string());
65 | let style = from_str(&css_content, &class);
66 | output_css += &style;
67 | }
68 | }
69 | }
70 | }
71 | }
72 | //todo: other than let statements cover that other way style! macro can instantiated.
73 | }
74 | }
75 | }
76 | }
77 |
78 | write_css(output_path, &output_css)
79 | .unwrap_or_else(|e| p!("Problem creating output file: {}", e.to_string()));
80 |
81 | p!(
82 | "{}",
83 | "===============================Stylers debug output end==============================="
84 | );
85 | }
86 |
87 | const OUTPUT_DIR: &str = "./target";
88 | /// Writes the styles in its own file and appends itself to the main.css file
89 | fn write_css(output_path: Option, content: &str) -> io::Result<()> {
90 | let mut out_path = String::from("./target/stylers_out.css");
91 | if let Some(path) = output_path {
92 | out_path = path;
93 | }
94 |
95 | fs::create_dir_all(OUTPUT_DIR)?;
96 |
97 | let mut buffer = File::create(out_path)?;
98 | buffer.write_all(content.as_bytes())?;
99 | buffer.flush()?;
100 |
101 | Ok(())
102 | }
103 |
--------------------------------------------------------------------------------
/stylers_core/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "stylers_core"
3 | version = "1.0.2"
4 | edition = "2021"
5 | authors = ["Abishek P"]
6 | license = "MIT"
7 | repository = "https://github.com/abishekatp/stylers"
8 | description = "Scoped CSS implementation in Rust"
9 | readme = "../README.md"
10 |
11 | [dependencies]
12 | quote = "1.0"
13 | proc-macro2 = { version = "1.0", features = ["span-locations"] }
14 | rand = "0.8.5"
15 | rand_chacha = "0.3.1"
16 | levenshtein = "1.0.5"
17 |
--------------------------------------------------------------------------------
/stylers_core/src/lib.rs:
--------------------------------------------------------------------------------
1 | #![feature(extend_one)]
2 | #![feature(proc_macro_span)]
3 | mod style;
4 | mod style_sheet;
5 |
6 | use proc_macro2::TokenStream;
7 | use quote::{quote, ToTokens};
8 | use rand::prelude::*;
9 | use rand_chacha::ChaCha8Rng;
10 | use std::collections::hash_map::RandomState;
11 | use std::hash::{BuildHasher, Hasher};
12 |
13 | pub use style::build_style_from_ts as from_ts;
14 | pub use style_sheet::build_style_from_str as from_str;
15 |
16 | //ref: https://rust-random.github.io/book/guide-seeding.html
17 |
18 | #[derive(Debug)]
19 | pub struct Class(String);
20 |
21 | impl Class {
22 | pub fn new(class: String) -> Self {
23 | Self(class)
24 | }
25 |
26 | pub fn random() -> Self {
27 | let hash = RandomState::new().build_hasher().finish();
28 |
29 | Self(format!("l-{}", &hash.to_string()[0..6]))
30 | }
31 |
32 | pub fn rand_class_from_seed(content: String) -> Self {
33 | let mut no_of_chars = 0;
34 | for ch in content.chars() {
35 | if !ch.is_whitespace() && !ch.is_whitespace() {
36 | no_of_chars += 1;
37 | }
38 | }
39 | let mut rng = ChaCha8Rng::seed_from_u64(no_of_chars);
40 | let hash = rng.gen::();
41 | Self(format!("l-{}", &hash.to_string()[0..6]))
42 | }
43 |
44 | pub fn as_name(&self) -> &str {
45 | &self.0
46 | }
47 |
48 | pub fn as_selector(&self) -> String {
49 | format!(".{}", self.0)
50 | }
51 | }
52 |
53 | impl ToTokens for Class {
54 | fn to_tokens(&self, tokens: &mut TokenStream) {
55 | let class = self.as_name();
56 | tokens.extend(quote! { #class })
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/stylers_core/src/style/css_at_rule.rs:
--------------------------------------------------------------------------------
1 | use proc_macro2::{Delimiter, TokenStream, TokenTree};
2 | use std::collections::HashSet;
3 |
4 | use crate::style::css_style_sheet::{Rule, StyleSheet};
5 | use crate::style::utils::{add_spaces, parse_group};
6 | use crate::Class;
7 |
8 | /// ref: https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule
9 | #[derive(Debug)]
10 | pub(crate) struct AtRule {
11 | //nested at-rule may contain one or more css rule block inside it.
12 | pub(crate) rules: Vec,
13 | pub(crate) at_rules: Vec,
14 | }
15 |
16 | impl AtRule {
17 | // This method will parse the at-rule tokenstream and return teh AtRule
18 | // HashMap will contain all unique selectors which may be nested inside at-rule.
19 | pub(crate) fn new(
20 | token_stream: TokenStream,
21 | class: &Class,
22 | is_proc_macro: bool,
23 | ) -> (AtRule, HashSet) {
24 | let mut css_at_rule = AtRule {
25 | rules: vec![],
26 | at_rules: vec![],
27 | };
28 | css_at_rule.parse(token_stream, class, is_proc_macro);
29 |
30 | (css_at_rule, HashSet::new())
31 | }
32 |
33 | // This css_text method will give the whole at-rule as single string value.
34 | // Note that we the calling function will be responsible for passing token stream of single at-rule at a time.
35 | pub(crate) fn css_text(&self) -> String {
36 | let mut text = String::new();
37 | //when we call parse method recursively it pushes at rule in order from inner most to outer most.
38 | self.at_rules.iter().rev().for_each(|r| {
39 | text.push_str(r);
40 | text.push('{');
41 | });
42 | //here we add the css_rules which are nested inside of at-rules one by one.
43 | if !self.rules.is_empty() {
44 | for css_rule in self.rules.iter() {
45 | match css_rule {
46 | Rule::StyleRule(style_rule) => text.push_str(&style_rule.css_text()),
47 | Rule::AtRule(at_rule) => text.push_str(&at_rule.css_text()),
48 | }
49 | }
50 | for _ in 0..self.at_rules.len() {
51 | text.push('}');
52 | }
53 | }
54 | //in case of regular at_rule remove all extra open braces added in the previous step.
55 | let text = text.trim_matches('{');
56 | text.to_string()
57 | }
58 |
59 | // This parse method will parse the at-rule tokn stream.
60 | // Note: this is recursive function it will handle nested at-rules.
61 | fn parse(
62 | &mut self,
63 | token_stream: TokenStream,
64 | class: &Class,
65 | is_proc_macro: bool,
66 | ) -> HashSet {
67 | let mut at_rule = String::new();
68 | let mut selectors = HashSet::new();
69 |
70 | let mut pre_line = 0;
71 | let mut pre_col = 0;
72 |
73 | for tt in token_stream {
74 | match tt {
75 | TokenTree::Group(t) => {
76 | //only if the delimiter is brace it will be either style-rule or at-rule definition
77 | if t.delimiter() == Delimiter::Brace {
78 | let mut new_ts = t.stream().into_iter().take(1);
79 | let mut is_at_rule = false;
80 | if let Some(TokenTree::Punct(at)) = new_ts.next() {
81 | if at.as_char() == '@' {
82 | is_at_rule = true;
83 | }
84 | }
85 |
86 | if at_rule.contains("@page")
87 | || at_rule.contains("@font-face")
88 | || at_rule.contains("keyframes")
89 | || at_rule.contains("@counter-style")
90 | || at_rule.contains("@font-feature-values")
91 | || at_rule.contains("@property")
92 | {
93 | // At-rules will not contain any nested css-rules. so we just parse that group as a string.
94 | at_rule.push_str(&parse_group(t, is_proc_macro));
95 | } else if is_at_rule {
96 | // If there is another inner at-rule
97 | self.parse(t.stream(), class, is_proc_macro);
98 | } else {
99 | // Each at-rule may contain one or more css rules nested inside of it.
100 | let (mut style_sheet, new_map) =
101 | StyleSheet::new(t.stream(), class, is_proc_macro);
102 | self.rules.append(&mut style_sheet.rules);
103 | selectors = new_map;
104 | }
105 | self.at_rules.push(at_rule);
106 | at_rule = String::new();
107 | } else {
108 | add_spaces(
109 | &mut at_rule,
110 | t.span(),
111 | &mut pre_line,
112 | &mut pre_col,
113 | is_proc_macro,
114 | );
115 | at_rule.push_str(&parse_group(t, is_proc_macro));
116 | }
117 | }
118 | TokenTree::Ident(t) => {
119 | add_spaces(
120 | &mut at_rule,
121 | t.span(),
122 | &mut pre_line,
123 | &mut pre_col,
124 | is_proc_macro,
125 | );
126 | at_rule.push_str(&t.to_string());
127 | }
128 | TokenTree::Literal(t) => {
129 | add_spaces(
130 | &mut at_rule,
131 | t.span(),
132 | &mut pre_line,
133 | &mut pre_col,
134 | is_proc_macro,
135 | );
136 | at_rule.push_str(&t.to_string());
137 | }
138 | TokenTree::Punct(t) => {
139 | let ch = t.as_char();
140 | add_spaces(
141 | &mut at_rule,
142 | t.span(),
143 | &mut pre_line,
144 | &mut pre_col,
145 | is_proc_macro,
146 | );
147 | at_rule.push(ch);
148 | // Regular at-rule ends with semicolon. there won't be any style declaration for this.
149 | if ch == ';' {
150 | self.at_rules.push(at_rule.clone());
151 | }
152 | }
153 | }
154 | }
155 |
156 | selectors
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/stylers_core/src/style/css_style_declar.rs:
--------------------------------------------------------------------------------
1 | use crate::style::utils::add_spaces;
2 | use levenshtein::levenshtein;
3 | use proc_macro2::{Delimiter, Group, TokenTree};
4 | use std::collections::HashMap;
5 |
6 | /// ref: https://www.w3schools.com/cssref/index.php
7 | static ALL_PROPERTIES: [&str; 555] = [
8 | "accent-color",
9 | "align-content",
10 | "align-items",
11 | "align-self",
12 | "all",
13 | "animation",
14 | "animation-delay",
15 | "animation-direction",
16 | "animation-duration",
17 | "animation-fill-mode",
18 | "animation-iteration-count",
19 | "animation-name",
20 | "animation-play-state",
21 | "animation-timing-function",
22 | "aspect-ratio",
23 | "backdrop-filter",
24 | "backface-visibility",
25 | "background",
26 | "background-attachment",
27 | "background-blend-mode",
28 | "background-clip",
29 | "background-color",
30 | "background-image",
31 | "background-origin",
32 | "background-position",
33 | "background-position-x",
34 | "background-position-y",
35 | "background-repeat",
36 | "background-size",
37 | "block-size",
38 | "border",
39 | "border-block",
40 | "border-block-color",
41 | "border-block-end-color",
42 | "border-block-end-style",
43 | "border-block-end-width",
44 | "border-block-start-color",
45 | "border-block-start-style",
46 | "border-block-start-width",
47 | "border-block-style",
48 | "border-block-width",
49 | "border-bottom",
50 | "border-bottom-color",
51 | "border-bottom-left-radius",
52 | "border-bottom-right-radius",
53 | "border-bottom-style",
54 | "border-bottom-width",
55 | "border-collapse",
56 | "border-color",
57 | "border-image",
58 | "border-image-outset",
59 | "border-image-repeat",
60 | "border-image-slice",
61 | "border-image-source",
62 | "border-image-width",
63 | "border-inline",
64 | "border-inline-color",
65 | "border-inline-end-color",
66 | "border-inline-end-style",
67 | "border-inline-end-width",
68 | "border-inline-start-color",
69 | "border-inline-start-style",
70 | "border-inline-start-width",
71 | "border-inline-style",
72 | "border-inline-width",
73 | "border-left",
74 | "border-left-color",
75 | "border-left-style",
76 | "border-left-width",
77 | "border-radius",
78 | "border-right",
79 | "border-right-color",
80 | "border-right-style",
81 | "border-right-width",
82 | "border-spacing",
83 | "border-style",
84 | "border-top",
85 | "border-top-color",
86 | "border-top-left-radius",
87 | "border-top-right-radius",
88 | "border-top-style",
89 | "border-top-width",
90 | "border-width",
91 | "bottom",
92 | "box-decoration-break",
93 | "box-reflect",
94 | "box-shadow",
95 | "box-sizing",
96 | "break-after",
97 | "break-before",
98 | "break-inside",
99 | "caption-side",
100 | "caret-color",
101 | "clear",
102 | "clip",
103 | "color",
104 | "column-count",
105 | "column-fill",
106 | "column-gap",
107 | "column-rule",
108 | "column-rule-color",
109 | "column-rule-style",
110 | "column-rule-width",
111 | "column-span",
112 | "column-width",
113 | "columns",
114 | "content",
115 | "counter-increment",
116 | "counter-reset",
117 | "cursor",
118 | "direction",
119 | "display",
120 | "empty-cells",
121 | "filter",
122 | "flex",
123 | "flex-basis",
124 | "flex-direction",
125 | "flex-flow",
126 | "flex-grow",
127 | "flex-shrink",
128 | "flex-wrap",
129 | "float",
130 | "font",
131 | "font-family",
132 | "font-feature-settings",
133 | "font-kerning",
134 | "font-language-override",
135 | "font-size",
136 | "font-size-adjust",
137 | "font-stretch",
138 | "font-style",
139 | "font-synthesis",
140 | "font-variant",
141 | "font-variant-alternates",
142 | "font-variant-caps",
143 | "font-variant-east-asian",
144 | "font-variant-ligatures",
145 | "font-variant-numeric",
146 | "font-variant-position",
147 | "font-weight",
148 | "gap",
149 | "grid",
150 | "grid-area",
151 | "grid-auto-columns",
152 | "grid-auto-flow",
153 | "grid-auto-rows",
154 | "grid-column",
155 | "grid-column-end",
156 | "grid-column-gap",
157 | "grid-column-start",
158 | "grid-gap",
159 | "grid-row",
160 | "grid-row-end",
161 | "grid-row-gap",
162 | "grid-row-start",
163 | "grid-template",
164 | "grid-template-areas",
165 | "grid-template-columns",
166 | "grid-template-rows",
167 | "hanging-punctuation",
168 | "height",
169 | "hyphens",
170 | "image-rendering",
171 | "inline-size",
172 | "inset",
173 | "inset-block",
174 | "inset-block-end",
175 | "inset-block-start",
176 | "inset-inline",
177 | "inset-inline-end",
178 | "inset-inline-start",
179 | "isolation",
180 | "justify-content",
181 | "justify-items",
182 | "justify-self",
183 | "left",
184 | "letter-spacing",
185 | "line-break",
186 | "line-height",
187 | "list-style",
188 | "list-style-image",
189 | "list-style-position",
190 | "list-style-type",
191 | "margin",
192 | "margin-block",
193 | "margin-block-end",
194 | "margin-block-start",
195 | "margin-bottom",
196 | "margin-inline",
197 | "margin-inline-end",
198 | "margin-inline-start",
199 | "margin-left",
200 | "margin-right",
201 | "margin-top",
202 | "mask",
203 | "mask-clip",
204 | "mask-composite",
205 | "mask-image",
206 | "mask-mode",
207 | "mask-origin",
208 | "mask-position",
209 | "mask-repeat",
210 | "mask-size",
211 | "mask-type",
212 | "max-height",
213 | "max-width",
214 | "@media",
215 | "max-block-size",
216 | "max-inline-size",
217 | "min-block-size",
218 | "min-inline-size",
219 | "min-height",
220 | "min-width",
221 | "mix-blend-mode",
222 | "object-fit",
223 | "object-position",
224 | "opacity",
225 | "order",
226 | "orphans",
227 | "outline",
228 | "outline-color",
229 | "outline-offset",
230 | "outline-style",
231 | "outline-width",
232 | "overflow",
233 | "overflow-anchor",
234 | "overflow-wrap",
235 | "overflow-x",
236 | "overflow-y",
237 | "overscroll-behavior",
238 | "overscroll-behavior-block",
239 | "overscroll-behavior-inline",
240 | "overscroll-behavior-x",
241 | "overscroll-behavior-y",
242 | "padding",
243 | "padding-block",
244 | "padding-block-end",
245 | "padding-block-start",
246 | "padding-bottom",
247 | "padding-inline",
248 | "padding-inline-end",
249 | "padding-inline-start",
250 | "padding-left",
251 | "padding-right",
252 | "padding-top",
253 | "page-break-after",
254 | "page-break-before",
255 | "page-break-inside",
256 | "paint-order",
257 | "perspective",
258 | "perspective-origin",
259 | "place-content",
260 | "place-items",
261 | "place-self",
262 | "pointer-events",
263 | "position",
264 | "quotes",
265 | "resize",
266 | "right",
267 | "rotate",
268 | "row-gap",
269 | "scale",
270 | "scrollbar-width",
271 | "scrollbar-color",
272 | "scroll-behavior",
273 | "scroll-margin",
274 | "scroll-margin-block",
275 | "scroll-margin-block-end",
276 | "scroll-margin-block-start",
277 | "scroll-margin-bottom",
278 | "scroll-margin-inline",
279 | "scroll-margin-inline-end",
280 | "scroll-margin-inline-start",
281 | "scroll-margin-left",
282 | "scroll-margin-right",
283 | "scroll-margin-top",
284 | "scroll-padding",
285 | "scroll-padding-block",
286 | "scroll-padding-block-end",
287 | "scroll-padding-block-start",
288 | "scroll-padding-bottom",
289 | "scroll-padding-inline",
290 | "scroll-padding-inline-end",
291 | "scroll-padding-inline-start",
292 | "scroll-padding-left",
293 | "scroll-padding-right",
294 | "scroll-padding-top",
295 | "scroll-snap-align",
296 | "scroll-snap-stop",
297 | "scroll-snap-type",
298 | "tab-size",
299 | "table-layout",
300 | "text-align",
301 | "text-align-last",
302 | "text-combine-upright",
303 | "text-decoration",
304 | "text-decoration-color",
305 | "text-decoration-line",
306 | "text-decoration-style",
307 | "text-decoration-thickness",
308 | "text-emphasis",
309 | "text-indent",
310 | "text-justify",
311 | "text-orientation",
312 | "text-overflow",
313 | "text-shadow",
314 | "text-transform",
315 | "text-underline-position",
316 | "top",
317 | "transform",
318 | "transform-origin",
319 | "transform-style",
320 | "transition",
321 | "transition-delay",
322 | "transition-duration",
323 | "transition-property",
324 | "transition-timing-function",
325 | "translate",
326 | "unicode-bidi",
327 | "user-select",
328 | "vertical-align",
329 | "visibility",
330 | "white-space",
331 | "widows",
332 | "width",
333 | "word-break",
334 | "word-spacing",
335 | "word-wrap",
336 | "writing-mode",
337 | "z-index",
338 | // SVG properties below
339 | "accent-height",
340 | "accumulate",
341 | "additive",
342 | "alignment-baseline",
343 | "alphabetic",
344 | "amplitude",
345 | "arabic-form",
346 | "ascent",
347 | "attributeName",
348 | "attributeType",
349 | "azimuth",
350 | "baseFrequency",
351 | "baseline-shift",
352 | "baseProfile",
353 | "bbox",
354 | "begin",
355 | "bias",
356 | "by",
357 | "calcMode",
358 | "cap-height",
359 | "class",
360 | "clipPathUnits",
361 | "clip-path",
362 | "clip-rule",
363 | "color-interpolation",
364 | "color-interpolation-filters",
365 | "color-profile",
366 | "color-rendering",
367 | "contentScriptType",
368 | "contentStyleType",
369 | "crossorigin",
370 | "cx",
371 | "cy",
372 | "d",
373 | "decelerate",
374 | "descent",
375 | "diffuseConstant",
376 | "divisor",
377 | "dominant-baseline",
378 | "dur",
379 | "dx",
380 | "dy",
381 | "edgeMode",
382 | "elevation",
383 | "enable-background",
384 | "end",
385 | "exponent",
386 | "fill",
387 | "fill-opacity",
388 | "fill-rule",
389 | "filterRes",
390 | "filterUnits",
391 | "flood-color",
392 | "flood-opacity",
393 | "format",
394 | "from",
395 | "fr",
396 | "fx",
397 | "fy",
398 | "g1",
399 | "g2",
400 | "glyph-name",
401 | "glyph-orientation-horizontal",
402 | "glyph-orientation-vertical",
403 | "glyphRef",
404 | "gradientTransform",
405 | "gradientUnits",
406 | "hanging",
407 | "href",
408 | "hreflang",
409 | "horiz-adv-x",
410 | "horiz-origin-x",
411 | "id",
412 | "ideographic",
413 | "in",
414 | "in2",
415 | "intercept",
416 | "k",
417 | "k1",
418 | "k2",
419 | "k3",
420 | "k4",
421 | "kernelMatrix",
422 | "kernelUnitLength",
423 | "kerning",
424 | "keyPoints",
425 | "keySplines",
426 | "keyTimes",
427 | "lang",
428 | "lengthAdjust",
429 | "lighting-color",
430 | "limitingConeAngle",
431 | "local",
432 | "marker-end",
433 | "marker-mid",
434 | "marker-start",
435 | "markerHeight",
436 | "markerUnits",
437 | "markerWidth",
438 | "maskContentUnits",
439 | "maskUnits",
440 | "mathematical",
441 | "max",
442 | "media",
443 | "method",
444 | "min",
445 | "mode",
446 | "name",
447 | "numOctaves",
448 | "offset",
449 | "operator",
450 | "orient",
451 | "orientation",
452 | "origin",
453 | "overline-position",
454 | "overline-thickness",
455 | "panose-1",
456 | "path",
457 | "pathLength",
458 | "patternContentUnits",
459 | "patternTransform",
460 | "patternUnits",
461 | "ping",
462 | "points",
463 | "pointsAtX",
464 | "pointsAtY",
465 | "pointsAtZ",
466 | "preserveAlpha",
467 | "preserveAspectRatio",
468 | "primitiveUnits",
469 | "r",
470 | "radius",
471 | "referrerPolicy",
472 | "refX",
473 | "refY",
474 | "rel",
475 | "rendering-intent",
476 | "repeatCount",
477 | "repeatDur",
478 | "requiredExtensions",
479 | "requiredFeatures",
480 | "restart",
481 | "result",
482 | "rx",
483 | "ry",
484 | "seed",
485 | "shape-rendering",
486 | "slope",
487 | "spacing",
488 | "specularConstant",
489 | "specularExponent",
490 | "speed",
491 | "spreadMethod",
492 | "startOffset",
493 | "stdDeviation",
494 | "stemh",
495 | "stemv",
496 | "stitchTiles",
497 | "stop-color",
498 | "stop-opacity",
499 | "strikethrough-position",
500 | "strikethrough-thickness",
501 | "string",
502 | "stroke",
503 | "stroke-dasharray",
504 | "stroke-dashoffset",
505 | "stroke-linecap",
506 | "stroke-linejoin",
507 | "stroke-miterlimit",
508 | "stroke-opacity",
509 | "stroke-width",
510 | "style",
511 | "surfaceScale",
512 | "systemLanguage",
513 | "tabindex",
514 | "tableValues",
515 | "target",
516 | "targetX",
517 | "targetY",
518 | "text-anchor",
519 | "text-rendering",
520 | "textLength",
521 | "transform-box",
522 | "to",
523 | "type",
524 | "u1",
525 | "u2",
526 | "underline-position",
527 | "underline-thickness",
528 | "unicode",
529 | "unicode-range",
530 | "units-per-em",
531 | "v-alphabetic",
532 | "v-hanging",
533 | "v-ideographic",
534 | "v-mathematical",
535 | "values",
536 | "vector-effect",
537 | "version",
538 | "vert-adv-y",
539 | "vert-origin-x",
540 | "vert-origin-y",
541 | "viewBox",
542 | "viewTarget",
543 | "widths",
544 | "x",
545 | "x-height",
546 | "x1",
547 | "x2",
548 | "xChannelSelector",
549 | "xlink:actuate",
550 | "xlink:arcrole",
551 | "xlink:role",
552 | "xlink:show",
553 | "xlink:title",
554 | "xlink:type",
555 | "xml:base",
556 | "xml:lang",
557 | "xml:space",
558 | "y",
559 | "y1",
560 | "y2",
561 | "yChannelSelector",
562 | "z",
563 | "zoomAndPan",
564 | ];
565 |
566 | /// ref: https://developer.mozilla.org/en-US/docs/Web/API/StyleDeclaration
567 | #[derive(Debug, Default)]
568 | pub(crate) struct StyleDeclaration {
569 | //e.g {color:red;}
570 | pub(crate) style_css_text: String,
571 | }
572 |
573 | impl StyleDeclaration {
574 | pub(crate) fn new(group: Group, is_proc_macro: bool) -> StyleDeclaration {
575 | let mut css_style_declar = StyleDeclaration {
576 | style_css_text: "".to_string(),
577 | };
578 | css_style_declar.parse(group, is_proc_macro);
579 | css_style_declar
580 | }
581 |
582 | pub(crate) fn style_css_text(&self) -> String {
583 | self.style_css_text.clone()
584 | }
585 |
586 | //parse and validate the style declaration group and store it in style_css_text.
587 | pub(crate) fn parse(&mut self, group: Group, is_proc_macro: bool) {
588 | let mut body = String::new();
589 | let mut property_map = HashMap::new();
590 | ALL_PROPERTIES.iter().for_each(|key| {
591 | property_map.insert(*key, ());
592 | });
593 | let mut property = String::new();
594 | let mut is_property_start = false;
595 | //if raw_str then we should not remove the double quotes
596 | let mut raw_str = false;
597 |
598 | let mut pre_col: usize = 0;
599 | let mut pre_line: usize = 0;
600 |
601 | body.push('{');
602 | group.stream().into_iter().for_each(|tt| match tt {
603 | TokenTree::Group(t) => {
604 | add_spaces(
605 | &mut property,
606 | t.span(),
607 | &mut pre_line,
608 | &mut pre_col,
609 | is_proc_macro,
610 | );
611 | property.push_str(&parse_property_group(t, raw_str, is_proc_macro));
612 | //completed parsing current raw_str group.
613 | raw_str = false;
614 | }
615 | TokenTree::Ident(t) => {
616 | add_spaces(
617 | &mut property,
618 | t.span(),
619 | &mut pre_line,
620 | &mut pre_col,
621 | is_proc_macro,
622 | );
623 | let ident = t.to_string();
624 | if ident == "raw_str" {
625 | raw_str = true;
626 | } else {
627 | property.push_str(&ident);
628 | }
629 | }
630 | TokenTree::Literal(t) => {
631 | add_spaces(
632 | &mut property,
633 | t.span(),
634 | &mut pre_line,
635 | &mut pre_col,
636 | is_proc_macro,
637 | );
638 | //we are trimming r and # because in some cases user have to use r#"\1g"34"#.
639 | //note: we will also trim all double quotes by default unless it is wrapped with raw_str()
640 | property.push_str(
641 | t.to_string()
642 | .trim_start_matches('r')
643 | .trim_matches(|c| c == '#' || c == '"'),
644 | );
645 | }
646 | TokenTree::Punct(t) => {
647 | let ch = t.as_char();
648 | //this will check if user added the semicolon or not.
649 | //since we are validating using colon actual line which is missing semicolon will be pre_line-1.
650 | if is_property_start && ch == ':' {
651 | panic!("Missing semicolon in line {}", pre_line - 1)
652 | } else if ch == ':' {
653 | is_property_start = true;
654 | let (is_valid, suggest) = validate_property(&property, &property_map);
655 | if !is_valid {
656 | panic!(
657 | "Did you mean to use {} property at line number {}",
658 | suggest.expect("Expected suggestion"),
659 | pre_line
660 | );
661 | }
662 | }
663 |
664 | add_spaces(
665 | &mut property,
666 | t.span(),
667 | &mut pre_line,
668 | &mut pre_col,
669 | is_proc_macro,
670 | );
671 | property.push(ch);
672 | //end of declaration of one property key value pair.
673 | if ch == ';' {
674 | body.push_str(&property);
675 | property = String::new();
676 | is_property_start = false;
677 | }
678 | }
679 | });
680 | body.push('}');
681 | self.style_css_text.push_str(&body);
682 | }
683 | }
684 |
685 | fn validate_property(prop_key: &str, prop_map: &HashMap<&str, ()>) -> (bool, Option) {
686 | let property = prop_key.strip_prefix("-webkit-").unwrap_or(prop_key);
687 | // Check if the property is a custom css property.
688 | if prop_map.contains_key(property) || property.starts_with("--") {
689 | return (true, None);
690 | }
691 | let mut most_relevent = String::new();
692 | let mut min_distance = 1000;
693 | ALL_PROPERTIES.iter().for_each(|key| {
694 | let dist = levenshtein(prop_key, key);
695 | if dist < min_distance {
696 | min_distance = dist;
697 | most_relevent = key.to_string();
698 | }
699 | });
700 | (false, Some(most_relevent))
701 | }
702 |
703 | fn parse_property_group(group: Group, raw_str: bool, is_proc_macro: bool) -> String {
704 | let mut body = String::new();
705 | let mut pre_col: usize = 0;
706 | let mut pre_line: usize = 0;
707 | let mut closing = ' ';
708 | //if raw_str then we should not remove the double quotes
709 | let mut raw_str = raw_str;
710 | match group.delimiter() {
711 | Delimiter::Brace => {
712 | body.push('{');
713 | closing = '}';
714 | }
715 | Delimiter::Parenthesis => {
716 | //there will be round bracket followed by raw_str() ident.
717 | if !raw_str {
718 | body.push('(');
719 | closing = ')';
720 | }
721 | }
722 | Delimiter::Bracket => {
723 | body.push('[');
724 | closing = ']';
725 | }
726 | _ => (),
727 | }
728 | group.stream().into_iter().for_each(|tt| match tt {
729 | TokenTree::Group(t) => {
730 | add_spaces(
731 | &mut body,
732 | t.span(),
733 | &mut pre_line,
734 | &mut pre_col,
735 | is_proc_macro,
736 | );
737 | let mut group_str: &str = &parse_property_group(t, raw_str, is_proc_macro);
738 | //there will be group token followed by raw_str! ident.
739 | if raw_str {
740 | group_str = group_str.trim_matches(|c| c == '(' || c == ')');
741 | //completed parsing current raw_str so set to false.
742 | raw_str = false;
743 | }
744 | body.push_str(group_str);
745 | }
746 | TokenTree::Ident(t) => {
747 | add_spaces(
748 | &mut body,
749 | t.span(),
750 | &mut pre_line,
751 | &mut pre_col,
752 | is_proc_macro,
753 | );
754 | let ident = t.to_string();
755 | if ident == "raw_str" {
756 | raw_str = true;
757 | }
758 | body.push_str(&ident);
759 | }
760 | TokenTree::Literal(t) => {
761 | add_spaces(
762 | &mut body,
763 | t.span(),
764 | &mut pre_line,
765 | &mut pre_col,
766 | is_proc_macro,
767 | );
768 | //in case of properties will trim r,# around the string literals
769 | let mut literal: &str = &t.to_string();
770 | literal = literal.trim_start_matches('r').trim_matches(|c| c == '#');
771 | if !raw_str {
772 | literal = literal.trim_matches('"');
773 | }
774 | body.push_str(literal);
775 | //completed parsing current raw_str so set to false.
776 | raw_str = false;
777 | }
778 | TokenTree::Punct(t) => {
779 | add_spaces(
780 | &mut body,
781 | t.span(),
782 | &mut pre_line,
783 | &mut pre_col,
784 | is_proc_macro,
785 | );
786 | body.push(t.as_char());
787 | }
788 | });
789 | body.push(closing);
790 | body.trim().to_string()
791 | }
792 |
--------------------------------------------------------------------------------
/stylers_core/src/style/css_style_rule.rs:
--------------------------------------------------------------------------------
1 | use proc_macro2::{Delimiter, TokenStream, TokenTree};
2 | use std::collections::HashSet;
3 |
4 | use crate::style::css_style_declar::StyleDeclaration;
5 | use crate::style::utils::{add_spaces, parse_group};
6 | use crate::Class;
7 |
8 | /// StyleRule is one kind of Rule which contains a selector text and a style declaration.
9 | /// Ressource:
10 | #[derive(Debug, Default)]
11 | pub(crate) struct StyleRule {
12 | pub(crate) selector_text: String,
13 | pub(crate) style: StyleDeclaration,
14 | }
15 |
16 | impl StyleRule {
17 | // This function will take the token stream of one StyleRule and parse it.
18 | // Note that we the calling function will be responsible for passing token stream of single style-rule at a time.
19 | pub(crate) fn new(
20 | ts: TokenStream,
21 | class: &Class,
22 | is_proc_macro: bool,
23 | ) -> (StyleRule, HashSet) {
24 | let mut css_style_rule = StyleRule::default();
25 | let sel_map = css_style_rule.parse(ts, class, is_proc_macro);
26 |
27 | (css_style_rule, sel_map)
28 | }
29 |
30 | // This css_text method will give the whole style-rule as single string value.
31 | pub(crate) fn css_text(&self) -> String {
32 | let mut text = self.selector_text.clone();
33 | text.push_str(&self.style.style_css_text());
34 | text
35 | }
36 |
37 | // parse method will extract the selector part of the style-rule and parse that selector using parse_selector method.
38 | fn parse(&mut self, ts: TokenStream, class: &Class, is_proc_macro: bool) -> HashSet {
39 | let mut pre_col: usize = 0;
40 | let mut pre_line: usize = 0;
41 | //selector will just store current selector of the style rule.
42 | let mut selector = String::new();
43 | let mut sel_map = HashSet::new();
44 |
45 | for tt in ts {
46 | match tt {
47 | TokenTree::Group(t) => {
48 | //only if the delimiter is brace it will be style definition.
49 | if t.delimiter() == Delimiter::Brace {
50 | sel_map = self.parse_selector(&selector, class);
51 | self.style = StyleDeclaration::new(t, is_proc_macro);
52 | } else {
53 | add_spaces(
54 | &mut selector,
55 | t.span(),
56 | &mut pre_line,
57 | &mut pre_col,
58 | is_proc_macro,
59 | );
60 | selector.push_str(&parse_group(t, is_proc_macro));
61 | }
62 | }
63 | TokenTree::Ident(t) => {
64 | add_spaces(
65 | &mut selector,
66 | t.span(),
67 | &mut pre_line,
68 | &mut pre_col,
69 | is_proc_macro,
70 | );
71 | selector.push_str(&t.to_string());
72 | }
73 | TokenTree::Literal(t) => {
74 | add_spaces(
75 | &mut selector,
76 | t.span(),
77 | &mut pre_line,
78 | &mut pre_col,
79 | is_proc_macro,
80 | );
81 | selector.push_str(t.to_string().trim_matches('"'));
82 | }
83 | TokenTree::Punct(t) => {
84 | let ch = t.as_char();
85 | //only when ch is dot or hash we need space information. because space will mean direct child.
86 | //colon also need space info because it may be custom directive like :deep(p)
87 | if ch == '.' || ch == '#' || ch == ':' {
88 | add_spaces(
89 | &mut selector,
90 | t.span(),
91 | &mut pre_line,
92 | &mut pre_col,
93 | is_proc_macro,
94 | );
95 | } else {
96 | let end = t.span().end();
97 | pre_col = end.column;
98 | pre_line = end.line;
99 | if is_proc_macro {
100 | let end = t.span().unwrap().end();
101 | pre_col = end.column();
102 | pre_line = end.line();
103 | }
104 | }
105 | selector.push(t.as_char());
106 | }
107 | }
108 | }
109 | sel_map
110 | }
111 |
112 | //parse_selector method will parse the all parts of selector and add random class to them
113 | pub(crate) fn parse_selector(&mut self, selector_text: &str, class: &Class) -> HashSet {
114 | let mut sel_map: HashSet = HashSet::new();
115 | let mut source = String::new();
116 | let sel_len = selector_text.len();
117 | let mut is_punct_start = false;
118 | let mut is_pseudo_class_start = false;
119 | let mut is_bracket_open = false;
120 | let mut is_deep_directive = false;
121 | let mut is_deep_directive_open = false;
122 | let mut was_deep_directive_open = false;
123 | let mut temp = String::new();
124 | let mut i = 0;
125 | for c in selector_text.chars() {
126 | i += 1;
127 | //when reading external files in h2,h1{} selector will be splitted into multiple lines
128 | if c == '\n' {
129 | continue;
130 | }
131 |
132 | //ignore all white spaces after :deep directive.
133 | if was_deep_directive_open {
134 | if c.is_whitespace() {
135 | source.push(' ');
136 | continue;
137 | } else {
138 | source.push(c);
139 | was_deep_directive_open = false;
140 | continue;
141 | }
142 | }
143 |
144 | //ignore everything between square brackets.
145 | //todo:handle the case when brackets inside attribute.
146 | if is_bracket_open {
147 | if c == ']' {
148 | is_bracket_open = false;
149 | source.push(c);
150 |
151 | if !is_deep_directive_open {
152 | source.push_str(&class.as_selector());
153 | }
154 |
155 | temp.push(c);
156 | sel_map.insert(temp.clone());
157 | temp = String::new();
158 | } else {
159 | source.push(c);
160 | temp.push(c);
161 | }
162 | continue;
163 | }
164 | if c == '[' {
165 | is_bracket_open = true;
166 | source.push(c);
167 | temp.push(c);
168 | continue;
169 | }
170 |
171 | // if current char is colon and previous char is space means that is custom :deep directive.
172 | // then just use whatever values inside :deep() directive.
173 | if is_deep_directive_open && c != ')' {
174 | source.push(c);
175 | continue;
176 | }
177 | if is_deep_directive_open && c == ')' {
178 | is_deep_directive = false;
179 | is_deep_directive_open = false;
180 | was_deep_directive_open = true;
181 | continue;
182 | }
183 | if is_deep_directive && c == '(' {
184 | is_deep_directive_open = true;
185 | continue;
186 | }
187 | if is_deep_directive && c != '(' {
188 | continue;
189 | }
190 | if c == ':' {
191 | if let Some(sub) = selector_text.get(i..i + 4) {
192 | if sub == "deep" {
193 | is_deep_directive = true;
194 | continue;
195 | }
196 | }
197 | }
198 |
199 | //ignore everything until we reach to whitespace or end of the line after encountering pseudo class selector(:).
200 | if is_pseudo_class_start {
201 | if c == ' ' || i == sel_len {
202 | source.push(c);
203 | is_pseudo_class_start = false;
204 |
205 | if c != ' ' {
206 | temp.push(c);
207 | }
208 | sel_map.insert(temp.clone());
209 | temp = String::new();
210 | } else {
211 | source.push(c);
212 | temp.push(c);
213 | }
214 | continue;
215 | }
216 | if c == ':' {
217 | is_pseudo_class_start = true;
218 | source.push_str(&class.as_selector());
219 | source.push(c);
220 | temp.push(c);
221 | continue;
222 | }
223 |
224 | //this condition ignores the unwanted white space after comma, >, +, ~ punctuations.
225 | if is_punct_start {
226 | // this will remove newline charactors following comma(,) in selectors.
227 | if c.is_whitespace() {
228 | continue;
229 | } else {
230 | is_punct_start = false;
231 | }
232 | }
233 | if c == ',' || c == '+' || c == '~' || c == '>' || c == '|' {
234 | source.push_str(&class.as_selector());
235 | source.push(c);
236 | is_punct_start = true;
237 |
238 | sel_map.insert(temp.clone());
239 | temp = String::new();
240 | continue;
241 | }
242 |
243 | //check for universal selector.
244 | if c == '*' {
245 | source.push_str(&class.as_selector());
246 | sel_map.insert("*".to_string());
247 | continue;
248 | }
249 |
250 | //append random class if we reach end of the line.
251 | if i == sel_len {
252 | source.push(c);
253 | source.push_str(&class.as_selector());
254 |
255 | temp.push(c);
256 | sel_map.insert(temp.clone());
257 | temp = String::new();
258 | continue;
259 | }
260 |
261 | //check for direct child selector
262 | if c == ' ' {
263 | source.push_str(&class.as_selector());
264 | source.push(' ');
265 |
266 | sel_map.insert(temp.clone());
267 | temp = String::new();
268 | } else {
269 | source.push(c);
270 | temp.push(c);
271 | }
272 | }
273 |
274 | //if :root pseudo element is used then no need to add random class.
275 | if source.contains(":root") {
276 | source = String::from(":root");
277 | }
278 | self.selector_text = source;
279 | sel_map
280 | }
281 | }
282 |
--------------------------------------------------------------------------------
/stylers_core/src/style/css_style_sheet.rs:
--------------------------------------------------------------------------------
1 | use proc_macro2::{Delimiter, TokenStream, TokenTree};
2 | use std::collections::HashSet;
3 |
4 | use crate::style::css_at_rule::AtRule;
5 | use crate::style::css_style_rule::StyleRule;
6 | use crate::Class;
7 |
8 | // Rule is enum which will have two kinds style-rule and at-rule(which begins with @)
9 | /// Ressource:
10 | #[derive(Debug)]
11 | pub(crate) enum Rule {
12 | StyleRule(StyleRule),
13 | AtRule(AtRule),
14 | }
15 |
16 | /// Ressources: and
17 | #[derive(Debug, Default)]
18 | pub(crate) struct StyleSheet {
19 | pub(crate) rules: Vec,
20 | }
21 |
22 | impl StyleSheet {
23 | pub(crate) fn new(
24 | token_stream: impl IntoIterator- ,
25 | class: &Class,
26 | is_proc_macro: bool,
27 | ) -> (StyleSheet, HashSet
) {
28 | let mut css_style_sheet = StyleSheet::default();
29 |
30 | let mut css_rule_tt = TokenStream::new();
31 | let mut sel_map = HashSet::new();
32 |
33 | let mut is_at_rule = false;
34 | let mut count = 0;
35 |
36 | for tt in token_stream {
37 | count += 1;
38 |
39 | css_rule_tt.extend(Some(tt.clone()));
40 |
41 | match tt {
42 | TokenTree::Group(t) => {
43 | if t.delimiter() == Delimiter::Brace {
44 | count = 0;
45 | if is_at_rule {
46 | let (at_rule, new_map) = AtRule::new(css_rule_tt, class, is_proc_macro);
47 | css_style_sheet.rules.push(Rule::AtRule(at_rule));
48 | sel_map.extend(new_map);
49 | is_at_rule = false;
50 | } else {
51 | let (style_rule, new_map) =
52 | StyleRule::new(css_rule_tt, class, is_proc_macro);
53 | css_style_sheet.rules.push(Rule::StyleRule(style_rule));
54 | sel_map.extend(new_map);
55 | }
56 | css_rule_tt = TokenStream::new();
57 | }
58 | }
59 | TokenTree::Punct(p) => {
60 | let ch = p.as_char();
61 | if ch == '@' && count == 1 {
62 | is_at_rule = true;
63 | }
64 | //in regular at-rule css rule ends with semicolon without any style declaration.
65 | if is_at_rule && ch == ';' {
66 | let (at_rule, new_map) = AtRule::new(css_rule_tt, class, is_proc_macro);
67 | css_style_sheet.rules.push(Rule::AtRule(at_rule));
68 | sel_map.extend(new_map);
69 | is_at_rule = false;
70 | css_rule_tt = TokenStream::new();
71 | }
72 | }
73 | _ => continue,
74 | }
75 | }
76 |
77 | (css_style_sheet, sel_map)
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/stylers_core/src/style/mod.rs:
--------------------------------------------------------------------------------
1 | //! This create as of now only exposes one function named build_style.
2 | //! The main focus of this function is to provide scoped css for Rust components(for the framework which provides component like architecture e.g leptos).
3 | //! This function can be used parse the style sheet in rust.
4 | mod css_at_rule;
5 | mod css_style_declar;
6 | mod css_style_rule;
7 | mod css_style_sheet;
8 | mod utils;
9 | use proc_macro2::TokenTree;
10 | use std::collections::HashSet;
11 |
12 | pub(crate) use crate::style::css_at_rule::AtRule;
13 | pub(crate) use crate::style::css_style_declar::StyleDeclaration;
14 | pub(crate) use crate::style::css_style_rule::StyleRule;
15 | pub(crate) use crate::style::css_style_sheet::{Rule, StyleSheet};
16 | use crate::Class;
17 |
18 | /// This function will build the whole style text as rust TokenStream.
19 | /// This function will take two arguments.
20 | /// ts: TokenStream which is token stream of text content of whole style sheet.
21 | /// random_class: &String is random class to be appended for each selector.
22 | /// This function will return tuple with two fields (style string, map of unique keys of selectors.)
23 | /// style string: is the parsed style sheet as a string
24 | pub fn build_style_from_ts(
25 | token_stream: impl Iterator- ,
26 | class: &Class,
27 | is_proc_macro: bool,
28 | ) -> (String, HashSet
) {
29 | let mut style = String::new();
30 |
31 | let (style_sheet, sel_map) = StyleSheet::new(token_stream, class, is_proc_macro);
32 |
33 | style_sheet.rules.iter().for_each(|rule| match rule {
34 | Rule::AtRule(at_rule) => style.push_str(&at_rule.css_text()),
35 | Rule::StyleRule(style_rule) => style.push_str(&style_rule.css_text()),
36 | });
37 |
38 | (style, sel_map)
39 | }
40 |
41 | #[cfg(test)]
42 | mod tests {
43 | use super::*;
44 | use quote::quote;
45 |
46 | // TODO: Span is only available outside procedural macro crate. workaround?
47 | // https://docs.rs/proc-macro2/latest/proc_macro2/struct.Span.html#method.unwrap
48 | #[test]
49 | #[ignore]
50 | fn simple_tag() {
51 | let input = quote! {
52 | div {
53 | border: 1px solid black;
54 | margin: 25px 50px 75px 100px;
55 | background-color: lightblue;
56 | }
57 | };
58 |
59 | let class = Class::new("test".into());
60 | let (style, _) = build_style_from_ts(input.into_iter(), &class, true);
61 | assert_eq!(style,"div.test {border: 1px solid black;margin: 25px 50px 75px 100px;background-color: lightblue;}");
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/stylers_core/src/style/utils.rs:
--------------------------------------------------------------------------------
1 | use proc_macro2::{Delimiter, Group, TokenTree};
2 |
3 | // This parse_group function will parse the TokenTree::Group and return a string.
4 | // This function will add at most one whitespace even if there are many whitespaces in actual tokenstream.
5 | pub(crate) fn parse_group(group: Group, is_proc_macro: bool) -> String {
6 | let mut body = String::new();
7 | let mut pre_col: usize = 0;
8 | let mut pre_line: usize = 0;
9 | let mut closing = ' ';
10 | match group.delimiter() {
11 | Delimiter::Brace => {
12 | body.push('{');
13 | closing = '}';
14 | }
15 | Delimiter::Parenthesis => {
16 | body.push('(');
17 | closing = ')';
18 | }
19 | Delimiter::Bracket => {
20 | body.push('[');
21 | closing = ']';
22 | }
23 | _ => (),
24 | }
25 | group.stream().into_iter().for_each(|tt| match tt {
26 | TokenTree::Group(t) => {
27 | add_spaces(
28 | &mut body,
29 | t.span(),
30 | &mut pre_line,
31 | &mut pre_col,
32 | is_proc_macro,
33 | );
34 | body.push_str(&parse_group(t, is_proc_macro));
35 | }
36 | TokenTree::Ident(t) => {
37 | add_spaces(
38 | &mut body,
39 | t.span(),
40 | &mut pre_line,
41 | &mut pre_col,
42 | is_proc_macro,
43 | );
44 | body.push_str(&t.to_string());
45 | }
46 | TokenTree::Literal(t) => {
47 | add_spaces(
48 | &mut body,
49 | t.span(),
50 | &mut pre_line,
51 | &mut pre_col,
52 | is_proc_macro,
53 | );
54 | //we are trimming r because in some cases like "\1g34" is not valid rust syntax.
55 | //in those places user have to use r"\1g34".
56 | body.push_str(t.to_string().trim_start_matches('r').trim_matches('#'));
57 | }
58 | TokenTree::Punct(t) => {
59 | add_spaces(
60 | &mut body,
61 | t.span(),
62 | &mut pre_line,
63 | &mut pre_col,
64 | is_proc_macro,
65 | );
66 | body.push(t.as_char());
67 | }
68 | });
69 | body.push(closing);
70 | body
71 | }
72 |
73 | //check if spaces needed to be appended
74 | //note: this function also reset the pre_line and pre_col to the cureent token's end line and column
75 | //note: this function convert proc_macro2::Span to proc_macro::Span
76 | // https://docs.rs/proc-macro2/latest/proc_macro2/struct.Span.html#method.start
77 | pub(crate) fn add_spaces(
78 | source: &mut String,
79 | span: proc_macro2::Span,
80 | pre_line: &mut usize,
81 | pre_col: &mut usize,
82 | is_proc_macro: bool,
83 | ) {
84 | let mut start_col = span.start().column;
85 | let mut start_line = span.start().line;
86 | let mut end_col = span.end().column;
87 | let mut end_line = span.end().line;
88 | if is_proc_macro {
89 | start_col = span.unwrap().start().column();
90 | start_line = span.unwrap().start().line();
91 | end_col = span.unwrap().end().column();
92 | end_line = span.unwrap().end().line();
93 | }
94 | let cur_col = start_col;
95 | let cur_line = start_line;
96 | if *pre_line == cur_line && cur_col > *pre_col {
97 | source.push(' ');
98 | }
99 | *pre_col = end_col;
100 | *pre_line = end_line;
101 | }
102 |
--------------------------------------------------------------------------------
/stylers_core/src/style_sheet/css_at_rule.rs:
--------------------------------------------------------------------------------
1 | use crate::style::AtRule;
2 | use crate::Class;
3 |
4 | use crate::style::StyleSheet;
5 |
6 | impl AtRule {
7 | // This method will parse the at-rule block string and return the AtRule
8 | // note: this is string version of the parse method in AtRule struct.
9 | pub(crate) fn from_str(at_block: &str, class: &Class) -> AtRule {
10 | let mut css_at_rule = AtRule {
11 | rules: vec![],
12 | at_rules: vec![],
13 | };
14 | css_at_rule.parse_from_str(at_block, class);
15 |
16 | css_at_rule
17 | }
18 |
19 | // This parse method will parse the at-rule block string.
20 | // note: this is string version of the parse method in AtRule struct.
21 | fn parse_from_str(&mut self, at_block: &str, class: &Class) {
22 | if at_block.trim().ends_with(';') {
23 | self.at_rules.push(parse_at_rule_declaration(at_block));
24 | } else {
25 | let mut at_block = at_block;
26 | loop {
27 | let (at_rule, declaration) = at_block.split_once('{').expect("Expecting At rule");
28 | //removing extra white spaces and extra closing braces at the end.
29 | let mut declaration = declaration.trim();
30 | let (first, _) = declaration
31 | .rsplit_once('}')
32 | .expect("Expecting to remove extra closing braces");
33 | declaration = first;
34 |
35 | //for some cases keyframes comes with prefix @-webkit-keyframes
36 | if at_rule.contains("@page")
37 | || at_rule.contains("@font-face")
38 | || at_rule.contains("keyframes")
39 | || at_rule.contains("@counter-style")
40 | || at_rule.contains("@font-feature-values")
41 | || at_rule.contains("@property")
42 | {
43 | let mut at_rule = at_rule.to_string();
44 | at_rule.push('{');
45 | at_rule.push_str(&parse_at_rule_declaration(declaration));
46 | at_rule.push('}');
47 | self.at_rules.push(at_rule.to_string());
48 | break;
49 | } else if declaration.starts_with('@') {
50 | self.at_rules.push(at_rule.to_string());
51 | at_block = declaration;
52 | continue;
53 | } else {
54 | self.at_rules.push(at_rule.to_string());
55 | let style_sheet = StyleSheet::from_str(declaration, class);
56 | self.rules = style_sheet.rules;
57 | break;
58 | }
59 | }
60 | }
61 | self.at_rules.reverse();
62 | }
63 | }
64 |
65 | //Some at rules don't contain another style rule inside them for those rule we can directly parse the string
66 | fn parse_at_rule_declaration(at_rule_declar: &str) -> String {
67 | let mut parts: Vec<&str> = at_rule_declar.split('\n').collect();
68 | parts = parts.iter().map(|item| item.trim()).collect();
69 | parts.join("")
70 | }
71 |
--------------------------------------------------------------------------------
/stylers_core/src/style_sheet/css_style_declar.rs:
--------------------------------------------------------------------------------
1 | use crate::style::StyleDeclaration;
2 |
3 | impl StyleDeclaration {
4 | // note: this is string version of the new method in StyleDeclaration struct.
5 | pub(crate) fn from_str(style_declar: String) -> StyleDeclaration {
6 | let mut css_style_declar = StyleDeclaration {
7 | style_css_text: "".to_string(),
8 | };
9 | css_style_declar.parse_from_str(style_declar);
10 | css_style_declar
11 | }
12 |
13 | // note: this is string version of the parse method in StyleDeclaration struct.
14 | fn parse_from_str(&mut self, style_delar: String) {
15 | //todo: what if newline is inside content property
16 | let mut declarations: Vec<&str> = style_delar.split('\n').collect();
17 | declarations = declarations.iter().map(|item| item.trim()).collect();
18 | self.style_css_text = declarations.join("");
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/stylers_core/src/style_sheet/css_style_rule.rs:
--------------------------------------------------------------------------------
1 | use crate::style::{StyleDeclaration, StyleRule};
2 | use crate::Class;
3 |
4 | impl StyleRule {
5 | // This function will take the style block string and parse it.
6 | // this function will only parse single stryle rule block
7 | pub(crate) fn from_str(style_block: &str, class: &Class) -> StyleRule {
8 | let mut css_style_rule = StyleRule {
9 | selector_text: String::new(),
10 | style: StyleDeclaration::default(),
11 | };
12 | css_style_rule.parse_str(style_block, class);
13 |
14 | css_style_rule
15 | }
16 |
17 | // parse method will extract the selector part of the style-rule and parse that selector using parse_selector method.
18 | fn parse_str(&mut self, style_block: &str, class: &Class) {
19 | //selector will just store current selector of the style rule.
20 | let (selector_text, body) = style_block.split_once('{').expect("Expecting selector");
21 | let selector_text = selector_text.trim();
22 | let _ = self.parse_selector(selector_text, class);
23 | let mut style_declar = String::from("{");
24 | style_declar.push_str(body);
25 | self.style = StyleDeclaration::from_str(style_declar);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/stylers_core/src/style_sheet/css_style_sheet.rs:
--------------------------------------------------------------------------------
1 | use crate::style::AtRule;
2 | use crate::style::StyleRule;
3 | use crate::style::{Rule, StyleSheet};
4 | use crate::Class;
5 |
6 | impl StyleSheet {
7 | // This function will take the whole stylesheet content as string and return CSSStyleSheet structure
8 | pub(crate) fn from_str(style_str: &str, class: &Class) -> StyleSheet {
9 | //removing all the comments in the css content.
10 | let mut style_str = style_str.to_string();
11 | while let Some((first, last)) = style_str.split_once("/*") {
12 | let mut temp = String::new();
13 | temp.push_str(first);
14 | let (_, end) = last
15 | .split_once("*/")
16 | .expect("Expecting to split the comment");
17 | temp.push_str(end);
18 | style_str = temp;
19 | }
20 |
21 | let mut css_style_sheet = StyleSheet { rules: vec![] };
22 | let mut is_at_rule = false;
23 | let mut style = String::new();
24 | let mut no_of_openings = 0;
25 | let mut no_of_closings = 0;
26 | for ch in style_str.chars() {
27 | //trimming the style because empty spaces at the beginning are not significant.
28 | if style.trim_start().is_empty() && ch == '@' {
29 | is_at_rule = true;
30 | }
31 | if ch == '{' {
32 | no_of_openings += 1;
33 | }
34 | if ch == '}' {
35 | no_of_closings += 1;
36 | }
37 | style.push(ch);
38 |
39 | // ending with semicolon means at rule without style declaration
40 | if ch == ';' && is_at_rule && no_of_openings == 0 {
41 | //to omit empty whitespaces.
42 | style = style.trim().to_string();
43 | let at_rule = AtRule::from_str(&style, class);
44 | css_style_sheet.rules.push(Rule::AtRule(at_rule));
45 | style = String::new();
46 | is_at_rule = false
47 | } else if ch == '}' && no_of_openings != 0 && no_of_openings == no_of_closings {
48 | //this else condition handle one block of at_rule or style rule from the whole style sheet content.
49 | //to omit empty whitespaces.
50 | style = style.trim().to_string();
51 | if is_at_rule {
52 | let at_rule = AtRule::from_str(&style, class);
53 | css_style_sheet.rules.push(Rule::AtRule(at_rule));
54 | } else {
55 | let style_rule = StyleRule::from_str(&style, class);
56 | css_style_sheet.rules.push(Rule::StyleRule(style_rule));
57 | }
58 | no_of_openings = 0;
59 | no_of_closings = 0;
60 | style = String::new();
61 | is_at_rule = false;
62 | }
63 | }
64 |
65 | css_style_sheet
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/stylers_core/src/style_sheet/mod.rs:
--------------------------------------------------------------------------------
1 | mod css_at_rule;
2 | mod css_style_declar;
3 | mod css_style_rule;
4 | mod css_style_sheet;
5 |
6 | use crate::{
7 | style::{Rule, StyleSheet},
8 | Class,
9 | };
10 |
11 | /// This function will build the whole style text as the String.
12 | /// This build_style is string version of the build_style method from style macro.
13 | pub fn build_style_from_str(style_str: &str, class: &Class) -> String {
14 | let mut style = String::new();
15 | let style_sheet = StyleSheet::from_str(style_str, class);
16 | style_sheet.rules.iter().for_each(|rule| match rule {
17 | Rule::AtRule(at_rule) => style.push_str(&at_rule.css_text()),
18 | Rule::StyleRule(style_rule) => style.push_str(&style_rule.css_text()),
19 | });
20 |
21 | style
22 | }
23 |
--------------------------------------------------------------------------------
/stylers_macro/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "stylers_macro"
3 | version = "1.0.2"
4 | edition = "2021"
5 | authors = ["Abishek P"]
6 | license = "MIT"
7 | repository = "https://github.com/abishekatp/stylers"
8 | description = "Scoped CSS for Rust web frameworks like leptos"
9 | readme = "../README.md"
10 |
11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
12 | [lib]
13 | proc_macro = true
14 |
15 | [dependencies]
16 | quote = "1.0"
17 | proc-macro2 = { version = "1.0" }
18 | litrs = "0.4.0"
19 | stylers_core = "1.0.2"
20 |
--------------------------------------------------------------------------------
/stylers_macro/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! This crate provides style macro for scoped css in rust web frameworks which follows component like architecture e.g Leptos.
2 | #![feature(proc_macro_span)]
3 | #![warn(clippy::panic, clippy::unwrap_used, clippy::expect_used, clippy::cargo)]
4 |
5 | use std::fs;
6 | use std::path::Path;
7 |
8 | use litrs::StringLit;
9 | use proc_macro2::{self, TokenStream, TokenTree};
10 | use quote::quote;
11 |
12 | use stylers_core::Class;
13 | use stylers_core::{from_str, from_ts};
14 |
15 | /// style macro take any valid css as input and returns a unique class name.
16 | /// For examples see:
17 | #[proc_macro]
18 | pub fn style(ts: proc_macro::TokenStream) -> proc_macro::TokenStream {
19 | let strval = ts.to_string();
20 | let class = Class::rand_class_from_seed(strval);
21 | let class = class.as_name();
22 | let expanded = quote! {
23 | #class
24 | };
25 | proc_macro::TokenStream::from(expanded)
26 | }
27 |
28 | /// style_sheet macro take css file path as a string input and returns a unique class name.
29 | /// For examples see:
30 | #[proc_macro]
31 | pub fn style_sheet(ts: proc_macro::TokenStream) -> proc_macro::TokenStream {
32 | let file_path = ts.to_string();
33 | let file_path = file_path.trim_matches('"');
34 | let css_content = std::fs::read_to_string(file_path).expect("Expected to read file");
35 | let class = Class::rand_class_from_seed(css_content.to_string());
36 | let class = class.as_name();
37 | let expanded = quote! {
38 | #class
39 | };
40 | proc_macro::TokenStream::from(expanded)
41 | }
42 |
43 | #[proc_macro]
44 | pub fn style_str(token_stream: proc_macro::TokenStream) -> proc_macro::TokenStream {
45 | let token_stream = TokenStream::from(token_stream).into_iter();
46 | let expanded = style_str_(token_stream).unwrap_or_else(|err| quote! { compile_error!(#err) });
47 | proc_macro::TokenStream::from(expanded)
48 | }
49 |
50 | fn style_str_(token_stream: impl Iterator- ) -> Result
{
51 | let class = Class::random();
52 | let (style, _selectors) = from_ts(token_stream, &class, true);
53 |
54 | Ok(quote! { (#class, #style) })
55 | }
56 |
57 | #[proc_macro]
58 | pub fn style_sheet_str(token_stream: proc_macro::TokenStream) -> proc_macro::TokenStream {
59 | let token_stream = TokenStream::from(token_stream).into_iter();
60 | let expanded =
61 | style_sheet_str_(token_stream).unwrap_or_else(|err| quote! { compile_error!(#err) });
62 | proc_macro::TokenStream::from(expanded)
63 | }
64 |
65 | fn style_sheet_str_(token_stream: impl Iterator- ) -> Result
{
66 | let tokens = &token_stream.collect::>();
67 | let &[TokenTree::Literal(path_literal)] = &tokens.as_slice() else {
68 | return Err("Expected only a string literal".to_string());
69 | };
70 |
71 | let path = StringLit::try_from(path_literal)
72 | .map_err(|err| format!("Expected a string literal: {}", err))?;
73 | let path = Path::new(path.value());
74 |
75 | let style_sheet_content = fs::read_to_string(path).map_err(|_| "Expected to read file")?;
76 |
77 | let class = Class::random();
78 | let style = from_str(&style_sheet_content, &class);
79 |
80 | Ok(quote! { (#class, #style) })
81 | }
82 |
83 | #[doc(hidden)]
84 | #[proc_macro]
85 | pub fn style_sheet_test(token_stream: proc_macro::TokenStream) -> proc_macro::TokenStream {
86 | let token_stream = TokenStream::from(token_stream).into_iter();
87 | let expanded =
88 | style_sheet_test_(token_stream).unwrap_or_else(|err| quote! { compile_error!(#err) });
89 | proc_macro::TokenStream::from(expanded)
90 | }
91 |
92 | fn style_sheet_test_(token_stream: impl Iterator- ) -> Result
{
93 | let tokens = &token_stream.collect::>();
94 | let &[TokenTree::Literal(path_literal)] = &tokens.as_slice() else {
95 | return Err("Expected only a string literal".to_string());
96 | };
97 |
98 | let path = StringLit::try_from(path_literal)
99 | .map_err(|err| format!("Expected a string literal: {}", err))?;
100 | let path = Path::new(path.value());
101 |
102 | let style_sheet_content = fs::read_to_string(path).map_err(|_| "Expected to read file")?;
103 |
104 | let class = Class::new("test".into());
105 | let style = from_str(&style_sheet_content, &class);
106 |
107 | Ok(quote! { #style })
108 | }
109 |
110 | #[doc(hidden)]
111 | #[proc_macro]
112 | pub fn style_test(token_stream: proc_macro::TokenStream) -> proc_macro::TokenStream {
113 | let token_stream = TokenStream::from(token_stream).into_iter();
114 | let expanded = style_test_(token_stream).unwrap_or_else(|err| quote! { compile_error!(#err) });
115 | proc_macro::TokenStream::from(expanded)
116 | }
117 |
118 | fn style_test_(token_stream: impl Iterator- ) -> Result
{
119 | let token_stream = token_stream.into_iter();
120 |
121 | let class = Class::new("test".into());
122 | let (style, _selectors) = from_ts(token_stream, &class, true);
123 |
124 | Ok(quote! { #style })
125 | }
126 |
--------------------------------------------------------------------------------
/stylers_macro/tests/samples/at_rules.css:
--------------------------------------------------------------------------------
1 | @charset "UTF-8";
2 | @import url("landscape.css") screen and (orientation: landscape);
3 | @namespace svg url("http://www.w3.org/2000/svg");
4 | @layer theme,
5 | layout,
6 | utilities;
7 |
8 | @supports (display: flex) {
9 | @media screen and (min-width: 900px) {
10 | article {
11 | display: flex;
12 | }
13 | }
14 | }
15 |
16 | @supports (display: flex) {
17 | .flex-container>* {
18 | text-shadow: 0 0 2px blue;
19 | float: none;
20 | }
21 |
22 | .flex-container {
23 | display: flex;
24 | }
25 | }
26 |
27 | @document url("https://www.example.com/") {
28 | h1 {
29 | color: green;
30 | }
31 | }
32 |
33 | @layer framework {
34 | @layer layout {
35 | p {
36 | margin-block: 1rem;
37 | font: 0.9em/1.2 Arial, Helvetica, sans-serif;
38 | content: "\hello";
39 | content: "\hello";
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/stylers_macro/tests/samples/basics.css:
--------------------------------------------------------------------------------
1 | .two {
2 | color: yellow;
3 | }
4 |
5 | /* This comment should be ignored */
6 | .two.one {
7 | color: yellow;
8 | }
9 |
10 | .two .one {
11 | color: yellow;
12 | /* This comment is also should be ignored */
13 | }
14 |
15 | #firstname {
16 | background-color: yellow;
17 | }
18 |
19 | * {
20 | background-color: yellow;
21 | }
22 |
23 | div {
24 | border: 1px solid black;
25 | margin: 25px 50px 75px 100px;
26 | background-color: lightblue;
27 | }
28 |
29 | div .one p {
30 | color: blue;
31 | }
32 |
33 | div.one p div {
34 | color: blue;
35 | }
36 |
37 | div #two {
38 | color: blue;
39 | }
40 |
41 | h2,
42 | a {
43 | color: purple;
44 | }
--------------------------------------------------------------------------------
/stylers_macro/tests/samples/custom_pseudo.css:
--------------------------------------------------------------------------------
1 | :deep(h3 div) {
2 | color: orange;
3 | }
4 |
5 | div :deep(h3) {
6 | color: orange;
7 | }
8 |
9 | div>:deep(h3) {
10 | color: orange;
11 | }
--------------------------------------------------------------------------------
/stylers_macro/tests/samples/pseudo.css:
--------------------------------------------------------------------------------
1 | .one:hover {
2 | background-color: green;
3 | }
4 |
5 | p::before {
6 | content: "Read this: ";
7 | }
8 |
9 | div:nth-child(2) {
10 | background-color: green;
11 | }
12 |
13 | p:lang(it) {
14 | background: yellow;
15 | }
16 |
17 | svg|a {}
18 |
19 | :not(body) {
20 | background: #ff0000;
21 | }
22 |
23 | :root {
24 | --blue: #1e90ff;
25 | }
26 |
27 | body {
28 | background-color: var(--blue);
29 | }
30 |
31 | #container {
32 | --first-color: #290;
33 | }
34 |
35 | #thirdParagraph {
36 | background-color: var(--first-color);
37 | color: var(--second-color);
38 | }
--------------------------------------------------------------------------------
/stylers_macro/tests/samples/relations.css:
--------------------------------------------------------------------------------
1 | div>p {
2 | background-color: yellow;
3 | }
4 |
5 | div+p {
6 | background-color: yellow;
7 | }
8 |
9 | p~ul {
10 | background: #ff0000;
11 | }
12 |
13 | a[target] {
14 | background-color: yellow;
15 | }
16 |
17 | a[title="I am ,testing"] {
18 | background-color: yellow;
19 | }
20 |
21 | [title~=flower] {
22 | background-color: yellow;
23 | }
24 |
25 | [lang|=en] {
26 | background-color: yellow;
27 | }
28 |
29 | div[class^="test"] {
30 | background-color: yellow;
31 | }
32 |
33 | div[class$=test] {
34 | background-color: yellow;
35 | }
36 |
37 | div [class$=test] {
38 | background-color: yellow;
39 | }
40 |
41 | div[class*="test"] {
42 | background-color: yellow;
43 | }
--------------------------------------------------------------------------------
/stylers_macro/tests/samples/special_at_rules.css:
--------------------------------------------------------------------------------
1 | @page {
2 | size: A4;
3 | margin: 10%;
4 |
5 | @top-left-corner {
6 | content: "Page " counter(page);
7 | }
8 | }
9 |
10 | @font-face {
11 | font-family: "Trickster";
12 | src: local("Trickster"),
13 | url("trickster-COLRv1.otf") format("opentype") tech(color-COLRv1), url("trickster-outline.otf") format("opentype"), url("trickster-outline.woff") format("woff");
14 | }
15 |
16 | @keyframes spin1 {
17 | to {
18 | -webkit-transform: rotate(360deg);
19 | }
20 | }
21 |
22 | @-webkit-keyframes spin2 {
23 | to {
24 | -webkit-transform: rotate(360deg);
25 | }
26 | }
27 |
28 | @counter-style thumbs {
29 | system: cyclic;
30 | symbols: "\1F44D";
31 | suffix: " ";
32 | }
33 |
34 | @font-feature-values Font One {
35 | @styleset {
36 | nice-style: 12;
37 | }
38 | }
39 |
40 | @property --property-name {
41 | syntax: "";
42 | inherits: false;
43 | initial-value: #c0ffee;
44 | }
--------------------------------------------------------------------------------
/stylers_macro/tests/style.rs:
--------------------------------------------------------------------------------
1 | // note: temporarily writing these tests. once find a way to test styler_core module we can discard this.
2 | // run this command cargo run inside styler_test folder.
3 | // Ref: https://www.w3schools.com/cssref/css_selectors.php
4 |
5 | use stylers_macro::style_test;
6 |
7 | #[test]
8 | fn test_1() {
9 | let style = style_test! {.two{
10 | // this comment should be ignored
11 | color: yellow;
12 | }
13 | };
14 | assert_eq!(style, ".two.test{color: yellow;}");
15 | }
16 |
17 | #[test]
18 | fn test_2() {
19 | let style = style_test! {
20 | .two.one {
21 | color: yellow;
22 | }
23 | };
24 | assert_eq!(style, ".two.one.test{color: yellow;}");
25 | }
26 |
27 | #[test]
28 | fn test_3() {
29 | let style = style_test! {
30 | .two .one{
31 | color: yellow;
32 | }
33 | };
34 | assert_eq!(style, ".two.test .one.test{color: yellow;}");
35 | }
36 |
37 | #[test]
38 | fn test_4() {
39 | let style = style_test! {
40 | #firstname{
41 | background-color: yellow;
42 | }
43 | };
44 | assert_eq!(style, "#firstname.test{background-color: yellow;}");
45 | }
46 |
47 | #[test]
48 | fn test_5() {
49 | // todo: decide weather all element should have the random classname inserted for this.
50 | let style = style_test! {
51 | *{
52 | background-color: yellow;
53 | }
54 | };
55 | assert_eq!(style, ".test{background-color: yellow;}");
56 | }
57 |
58 | #[test]
59 | fn test_6() {
60 | let style = style_test! {
61 | div{
62 | border: 1px solid black;
63 | margin: 25px 50px 75px 100px;
64 | background-color: lightblue;
65 | }
66 | };
67 | assert_eq!(style,"div.test{border: 1px solid black;margin: 25px 50px 75px 100px;background-color: lightblue;}");
68 | }
69 | #[test]
70 | fn test_7() {
71 | let style = style_test! {
72 | div .one p{
73 | color: blue;
74 | }
75 | };
76 | assert_eq!(style, "div.test .one.test p.test{color: blue;}");
77 | }
78 |
79 | #[test]
80 | fn test_8() {
81 | let style = style_test! {
82 | div.one p div{
83 | color: blue;
84 | }
85 | };
86 | assert_eq!(style, "div.one.test p.test div.test{color: blue;}");
87 | }
88 |
89 | #[test]
90 | fn test_9() {
91 | let style = style_test! {
92 | div #two{
93 | color: blue;
94 | }
95 | };
96 | assert_eq!(style, "div.test #two.test{color: blue;}");
97 | }
98 |
99 | #[test]
100 | fn test_10() {
101 | let style = style_test! {
102 | h2 , a{
103 | color: purple;
104 | }
105 | };
106 | assert_eq!(style, "h2.test,a.test{color: purple;}");
107 | }
108 |
109 | #[test]
110 | fn test_11() {
111 | let style = style_test! {
112 | div > p{
113 | background-color: yellow;
114 | }
115 | };
116 | assert_eq!(style, "div.test>p.test{background-color: yellow;}");
117 | }
118 |
119 | #[test]
120 | fn test_12() {
121 | let style = style_test! {
122 | div + p {
123 | background-color: yellow;
124 | }
125 | };
126 | assert_eq!(style, "div.test+p.test{background-color: yellow;}");
127 | }
128 |
129 | #[test]
130 | fn test_13() {
131 | let style = style_test! {
132 | p ~ ul {
133 | background: #ff0000;
134 | }
135 | };
136 | assert_eq!(style, "p.test~ul.test{background: #ff0000;}");
137 | }
138 |
139 | #[test]
140 | fn test_14() {
141 | let style = style_test! {
142 | a[target] {
143 | background-color: yellow;
144 | }
145 | };
146 | assert_eq!(style, "a[target].test{background-color: yellow;}");
147 | }
148 |
149 | #[test]
150 | fn test_15() {
151 | let style = style_test! {
152 | a[title="I am ,testing"] {
153 | background-color: yellow;
154 | }
155 | };
156 | assert_eq!(
157 | style,
158 | r#"a[title="I am ,testing"].test{background-color: yellow;}"#
159 | );
160 | }
161 |
162 | #[test]
163 | fn test_16() {
164 | let style = style_test! {
165 | [title~=flower] {
166 | background-color: yellow;
167 | }
168 | };
169 | assert_eq!(style, "[title~=flower].test{background-color: yellow;}");
170 | }
171 |
172 | #[test]
173 | fn test_17() {
174 | let style = style_test! {
175 | [lang|=en] {
176 | background-color: yellow;
177 | }
178 | };
179 | assert_eq!(style, "[lang|=en].test{background-color: yellow;}");
180 | }
181 |
182 | #[test]
183 | fn test_18() {
184 | let style = style_test! {
185 | div[class^="test"] {
186 | background-color: yellow;
187 | }
188 | };
189 | assert_eq!(
190 | style,
191 | r#"div[class^="test"].test{background-color: yellow;}"#
192 | );
193 | }
194 |
195 | #[test]
196 | fn test_19() {
197 | let style = style_test! {
198 | div[class$=test] {
199 | background-color: yellow;
200 | }
201 | };
202 | assert_eq!(style, "div[class$=test].test{background-color: yellow;}");
203 | }
204 |
205 | #[test]
206 | fn test_20() {
207 | let style = style_test! {
208 | div [class$=test] {
209 | background-color: yellow;
210 | }
211 | };
212 | assert_eq!(
213 | style,
214 | "div.test [class$=test].test{background-color: yellow;}"
215 | );
216 | }
217 |
218 | #[test]
219 | fn test_21() {
220 | let style = style_test! {
221 | div[class*="test"] {
222 | background-color: yellow;
223 | }
224 | };
225 | assert_eq!(
226 | style,
227 | r#"div[class*="test"].test{background-color: yellow;}"#
228 | );
229 | }
230 |
231 | #[test]
232 | fn test_22() {
233 | let style = style_test! {
234 | .one:hover{
235 | background-color: green;
236 | }
237 | };
238 | assert_eq!(style, ".one.test:hover{background-color: green;}");
239 | }
240 |
241 | #[test]
242 | fn test_23() {
243 | let style = style_test! {
244 | p::before {
245 | content: raw_str("Read this: ");
246 | }
247 | };
248 | assert_eq!(style, r#"p.test::before{content: "Read this: ";}"#);
249 | }
250 |
251 | #[test]
252 | fn test_24() {
253 | let style = style_test! {
254 | div:nth-child(2){
255 | background-color: green;
256 | }
257 | };
258 | assert_eq!(style, "div.test:nth-child(2){background-color: green;}");
259 | }
260 |
261 | #[test]
262 | fn test_25() {
263 | let style = style_test! {
264 | p:lang(it){
265 | background: yellow;
266 | }
267 | };
268 | assert_eq!(style, "p.test:lang(it){background: yellow;}");
269 | }
270 |
271 | #[test]
272 | fn test_26() {
273 | let style = style_test! {
274 | svg|a {
275 | }
276 | };
277 | assert_eq!(style, "svg.test|a.test{}");
278 |
279 | //Regular at-rules
280 | }
281 |
282 | #[test]
283 | fn test_27() {
284 | let style = style_test! {
285 | @charset "UTF-8";
286 | };
287 | assert_eq!(style, r#"@charset "UTF-8";"#);
288 | }
289 |
290 | #[test]
291 | fn test_28() {
292 | let style = style_test! {
293 | @import url("landscape.css") screen and (orientation: landscape);
294 | };
295 | assert_eq!(
296 | style,
297 | r#"@import url("landscape.css") screen and (orientation: landscape);"#
298 | );
299 |
300 | //note: this is one of restriction since url contains "//" it cannot be mentioned without double quotes
301 | }
302 |
303 | #[test]
304 | fn test_29() {
305 | let style = style_test! {
306 | @namespace svg url("http://www.w3.org/2000/svg");
307 | };
308 | assert_eq!(
309 | style,
310 | r#"@namespace svg url("http://www.w3.org/2000/svg");"#
311 | );
312 |
313 | //nested at-rules
314 | }
315 |
316 | #[test]
317 | fn test_30() {
318 | let style = style_test! {
319 | @supports (display: flex) {
320 | @media screen and (min-width: 900px) {
321 | article {
322 | display: flex;
323 | }
324 | }
325 | }
326 | };
327 | assert_eq!(
328 | style,
329 | "@supports (display: flex){@media screen and (min-width: 900px){article.test{display: flex;}}}"
330 | );
331 | }
332 |
333 | #[test]
334 | fn test_31() {
335 | let style = style_test! {
336 | @document url("https://www.example.com/")
337 | {
338 | h1 {
339 | color: green;
340 | }
341 | }
342 | };
343 | assert_eq!(
344 | style,
345 | r#"@document url("https://www.example.com/"){h1.test{color: green;}}"#
346 | );
347 | }
348 |
349 | #[test]
350 | fn test_32() {
351 | let style = style_test! {
352 | @page {
353 | size: A4;
354 | margin: 10%;
355 |
356 | @top-left-corner {
357 | content: "Page " counter(page);
358 | }
359 | }
360 | };
361 | assert_eq!(
362 | style,
363 | r#"@page{size: A4;margin: 10%;@top-left-corner {content: "Page " counter(page);}}"#
364 | );
365 | }
366 |
367 | #[test]
368 | fn test_33() {
369 | let style = style_test! {
370 | @font-face {
371 | font-family: "Trickster";
372 | src: local("Trickster"),
373 | url("trickster-COLRv1.otf") format("opentype") tech(color-COLRv1), url("trickster-outline.otf")
374 | format("opentype"), url("trickster-outline.woff") format("woff");
375 | }
376 | };
377 | assert_eq!(
378 | style,
379 | r#"@font-face{font-family: "Trickster";src: local("Trickster"),url("trickster-COLRv1.otf") format("opentype") tech(color-COLRv1), url("trickster-outline.otf")format("opentype"), url("trickster-outline.woff") format("woff");}"#
380 | );
381 |
382 | // todo: currently we not adding any random string to keyframe identifier.
383 | //it is users responsibility to make these identifiers unique globaly.
384 | }
385 |
386 | #[test]
387 | fn test_34() {
388 | let style = style_test! {
389 | @keyframes spin1 {
390 | to {
391 | -webkit-transform: rotate(360deg);
392 | }
393 | }
394 | };
395 | assert_eq!(
396 | style,
397 | "@keyframes spin1{to {-webkit-transform: rotate(360deg);}}"
398 | );
399 | }
400 |
401 | #[test]
402 | fn test_35() {
403 | let style = style_test! {
404 | @-webkit-keyframes spin2 {
405 | to {
406 | -webkit-transform: rotate(360deg);
407 | }
408 | }
409 | };
410 | assert_eq!(
411 | style,
412 | "@-webkit-keyframes spin2{to {-webkit-transform: rotate(360deg);}}"
413 | );
414 |
415 | //note: here we have to declare raw string because of backslash charactor
416 | }
417 |
418 | #[test]
419 | fn test_36() {
420 | let style = style_test! {
421 | @counter-style thumbs {
422 | system: cyclic;
423 | symbols: r"\1F44D";
424 | suffix: " ";
425 | }
426 | };
427 | assert_eq!(
428 | style,
429 | r#"@counter-style thumbs{system: cyclic;symbols: "\1F44D";suffix: " ";}"#
430 | );
431 | }
432 |
433 | #[test]
434 | fn test_37() {
435 | let style = style_test! {
436 | @font-feature-values Font One {
437 | @styleset {
438 | nice-style: 12;
439 | }
440 | }
441 | };
442 | assert_eq!(
443 | style,
444 | r#"@font-feature-values Font One{@styleset {nice-style: 12;}}"#
445 | );
446 |
447 | //note: this is experimental css rule.
448 | }
449 |
450 | #[test]
451 | fn test_38() {
452 | let style = style_test! {
453 | @property --property-name {
454 | syntax: "";
455 | inherits: false;
456 | initial-value: #c0ffee;
457 | }
458 | };
459 | assert_eq!(
460 | style,
461 | r#"@property --property-name{syntax: "";inherits: false;initial-value: #c0ffee;}"#
462 | );
463 |
464 | //note: when string literal is used as a value internally we will remove that double quotes unless it is wrapped with raw_str().
465 | }
466 |
467 | #[test]
468 | fn test_39() {
469 | let style = style_test! {
470 | @layer framework {
471 | @layer layout {
472 | p {
473 | margin-block: 1rem;
474 | font: "0.9em/1.2" Arial, Helvetica, sans-serif;
475 | content: raw_str(r"\hello");
476 | content: raw_str(r#"\hello"#);
477 | }
478 | }
479 | }
480 | };
481 | assert_eq!(
482 | style,
483 | r#"@layer framework{@layer layout{p.test{margin-block: 1rem;font: 0.9em/1.2 Arial, Helvetica, sans-serif;content: "\hello";content: "\hello";}}}"#
484 | );
485 | }
486 |
487 | #[test]
488 | fn test_40() {
489 | let style = style_test! {
490 | @layer theme, layout, utilities;
491 | };
492 | assert_eq!(style, r#"@layer theme, layout, utilities;"#);
493 | }
494 |
495 | #[test]
496 | fn test_41() {
497 | let style = style_test! {
498 | :not(body) {
499 | background: #ff0000;
500 | }
501 | };
502 | assert_eq!(style, ".test:not(body){background: #ff0000;}");
503 | }
504 |
505 | #[test]
506 | fn test_42() {
507 | let style = style_test! {
508 | :root {
509 | --blue: #1e90ff;
510 | }
511 |
512 | body { background-color: var(--blue); }
513 | };
514 | assert_eq!(
515 | style,
516 | ":root{--blue: #1e90ff;}body.test{background-color: var(--blue);}"
517 | );
518 | }
519 |
520 | #[test]
521 | fn test_43() {
522 | let style = style_test! {
523 | #container {
524 | --first-color: #290;
525 | }
526 | #thirdParagraph {
527 | background-color: var(--first-color);
528 | color: var(--second-color);
529 | }
530 | };
531 | assert_eq!(
532 | style,
533 | "#container.test{--first-color: #290;}#thirdParagraph.test{background-color: var(--first-color);color: var(--second-color);}"
534 | );
535 | }
536 |
537 | #[test]
538 | fn test_44() {
539 | let style = style_test! {
540 | table th,
541 | table td {
542 | color: red;
543 | }
544 | };
545 | assert_eq!(style, "table.test th.test,table.test td.test{color: red;}");
546 |
547 | // Custom pseudo class.
548 | }
549 |
550 | #[test]
551 | fn test_45() {
552 | let style = style_test! {
553 | div :deep(h3) {
554 | color: orange;
555 | }
556 | };
557 | assert_eq!(style, "div.test h3{color: orange;}");
558 | }
559 |
560 | #[test]
561 | fn test_46() {
562 | let style = style_test! {
563 | :deep(h3 div) {
564 | color: orange;
565 | }
566 | };
567 | assert_eq!(style, "h3 div{color: orange;}");
568 | }
569 |
570 | #[test]
571 | fn test_47() {
572 | let style = style_test! {
573 | div> :deep(h3) {
574 | color: orange;
575 | }
576 | };
577 | assert_eq!(style, "div.test>h3{color: orange;}");
578 | }
579 |
580 | #[test]
581 | fn test_48() {
582 | let style = style_test! {
583 | :deep([data-custom]) {
584 | color: orange;
585 | }
586 | };
587 | assert_eq!(style, "[data-custom]{color: orange;}");
588 | }
589 |
590 | #[test]
591 | fn test_49() {
592 | let style = style_test! {
593 | .nested> :deep([data-custom]) {
594 | color: orange;
595 | }
596 | };
597 | assert_eq!(style, ".nested.test>[data-custom]{color: orange;}");
598 | }
599 |
600 | #[test]
601 | fn test_50() {
602 | let style = style_test! {
603 | @supports (display: flex) {
604 | .flex-container > * {
605 | text-shadow: 0 0 2px blue;
606 | float: none;
607 | }
608 |
609 | .flex-container {
610 | display: flex;
611 | }
612 | }
613 | };
614 | assert_eq!(
615 | style,
616 | "@supports (display: flex){.flex-container.test>.test{text-shadow: 0 0 2px blue;float: none;}.flex-container.test{display: flex;}}"
617 | );
618 | }
619 |
620 | #[test]
621 | fn test_51() {
622 | let style = style_test! {
623 | :deep(.rollUp) .unlockSplash {
624 | max-height: 0;
625 | }
626 | };
627 | assert_eq!(style, ".rollUp .unlockSplash.test{max-height: 0;}");
628 | }
629 |
630 | #[test]
631 | fn test_52() {
632 | let style = style_test! {
633 | .unitToggle :deep(.onDisplay),
634 | .unitToggle :deep(.offDisplay) {
635 | color: black;
636 | }
637 | };
638 | assert_eq!(
639 | style,
640 | ".unitToggle.test .onDisplay,.unitToggle.test .offDisplay{color: black;}"
641 | );
642 | }
643 |
644 | #[test]
645 | fn test_53() {
646 | let style = style_test! {
647 | .wingman :deep(svg[role=graphics-symbol]) {
648 | width: 100%;
649 | }
650 | };
651 | assert_eq!(
652 | style,
653 | ".wingman.test svg[role=graphics-symbol]{width: 100%;}"
654 | );
655 | }
656 |
657 | #[test]
658 | fn test_54() {
659 | let style = style_test! {
660 | .errorSign {
661 | transform-box: fill-box;
662 | transform-origin: center;
663 | scrollbar-width: 1px;
664 | scrollbar-color: red;
665 | }
666 | };
667 | assert_eq!(
668 | style,
669 | ".errorSign.test{transform-box: fill-box;transform-origin: center;scrollbar-width: 1px;scrollbar-color: red;}"
670 | );
671 | }
672 |
--------------------------------------------------------------------------------
/stylers_macro/tests/style_sheet.rs:
--------------------------------------------------------------------------------
1 | use stylers_macro::style_sheet_test;
2 |
3 | // note: to check why test cases are failing we can use the text compare tools to compare and see the differences between left and right values of a test case
4 | // note: temporarily writing these tests. once find a way to test styler_core module we can discard this.
5 | // run this command cargo run inside styler_test folder.
6 | // Ref: https://www.w3schools.com/cssref/css_selectors.php
7 | #[test]
8 | pub fn basic() {
9 | let style = style_sheet_test!("stylers_macro/tests/samples/basics.css");
10 |
11 | assert_eq!(
12 | style,
13 | ".two.test{color: yellow;}.two.one.test{color: yellow;}.two.test .one.test{color: yellow;}#firstname.test{background-color: yellow;}.test{background-color: yellow;}div.test{border: 1px solid black;margin: 25px 50px 75px 100px;background-color: lightblue;}div.test .one.test p.test{color: blue;}div.one.test p.test div.test{color: blue;}div.test #two.test{color: blue;}h2.test,a.test{color: purple;}"
14 | );
15 | }
16 |
17 | #[test]
18 | pub fn relation() {
19 | let style = style_sheet_test!("stylers_macro/tests/samples/relations.css");
20 |
21 | assert_eq!(
22 | style,
23 | r#"div.test>p.test{background-color: yellow;}div.test+p.test{background-color: yellow;}p.test~ul.test{background: #ff0000;}a[target].test{background-color: yellow;}a[title="I am ,testing"].test{background-color: yellow;}[title~=flower].test{background-color: yellow;}[lang|=en].test{background-color: yellow;}div[class^="test"].test{background-color: yellow;}div[class$=test].test{background-color: yellow;}div.test [class$=test].test{background-color: yellow;}div[class*="test"].test{background-color: yellow;}"#
24 | );
25 | }
26 |
27 | #[test]
28 | fn pseudo() {
29 | let style = style_sheet_test!("stylers_macro/tests/samples/pseudo.css");
30 |
31 | assert_eq!(
32 | style,
33 | r#".one.test:hover{background-color: green;}p.test::before{content: "Read this: ";}div.test:nth-child(2){background-color: green;}p.test:lang(it){background: yellow;}svg.test|a.test{}.test:not(body){background: #ff0000;}:root{--blue: #1e90ff;}body.test{background-color: var(--blue);}#container.test{--first-color: #290;}#thirdParagraph.test{background-color: var(--first-color);color: var(--second-color);}"#
34 | );
35 | }
36 |
37 | #[test]
38 | fn at_rules() {
39 | let style = style_sheet_test!("stylers_macro/tests/samples/at_rules.css");
40 | assert_eq!(
41 | style,
42 | r#"@charset "UTF-8";@import url("landscape.css") screen and (orientation: landscape);@namespace svg url("http://www.w3.org/2000/svg");@layer theme,layout,utilities;@supports (display: flex) {@media screen and (min-width: 900px) {article.test{display: flex;}}}@supports (display: flex) {.flex-container.test>.test{text-shadow: 0 0 2px blue;float: none;}.flex-container.test{display: flex;}}@document url("https://www.example.com/") {h1.test{color: green;}}@layer framework {@layer layout {p.test{margin-block: 1rem;font: 0.9em/1.2 Arial, Helvetica, sans-serif;content: "\hello";content: "\hello";}}}"#
43 | );
44 | }
45 |
46 | #[test]
47 | fn special_at_rules() {
48 | let style = style_sheet_test!("stylers_macro/tests/samples/special_at_rules.css");
49 |
50 | assert_eq!(
51 | style,
52 | r#"@page {size: A4;margin: 10%;@top-left-corner {content: "Page " counter(page);}}@font-face {font-family: "Trickster";src: local("Trickster"),url("trickster-COLRv1.otf") format("opentype") tech(color-COLRv1), url("trickster-outline.otf") format("opentype"), url("trickster-outline.woff") format("woff");}@keyframes spin1 {to {-webkit-transform: rotate(360deg);}}@-webkit-keyframes spin2 {to {-webkit-transform: rotate(360deg);}}@counter-style thumbs {system: cyclic;symbols: "\1F44D";suffix: " ";}@font-feature-values Font One {@styleset {nice-style: 12;}}@property --property-name {syntax: "";inherits: false;initial-value: #c0ffee;}"#
53 | );
54 | }
55 |
56 | #[test]
57 | fn custom_pseudo_class() {
58 | let style = style_sheet_test!("stylers_macro/tests/samples/custom_pseudo.css");
59 |
60 | assert_eq!(
61 | style,
62 | r#"h3 div{color: orange;}div.test h3{color: orange;}div.test>h3{color: orange;}"#
63 | );
64 | }
65 |
--------------------------------------------------------------------------------