├── .gitattributes ├── .github └── workflows │ ├── audit-nightly.yml │ ├── audit-on-push.yml │ ├── ci.yml │ └── lint.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── dev └── bin │ ├── get-categories.pl │ └── install-dev-tools.sh ├── generator ├── Cargo.toml ├── Changes.md ├── README.md └── src │ └── main.rs ├── git ├── hooks │ └── pre-commit.sh └── setup.pl ├── integration ├── Cargo.toml └── tests │ └── lib.rs ├── macros ├── Cargo.toml ├── Changes.md ├── README.md ├── examples │ ├── css │ │ ├── generated.rs │ │ └── mod.rs │ └── main.rs └── src │ ├── lib.rs │ └── to_option_vec_string.rs ├── precious.toml └── test-project ├── Cargo.lock ├── Cargo.toml ├── assets └── tailwind_compiled.css ├── css └── tailwind.css ├── regen.sh ├── src ├── generated.rs └── main.rs └── tailwind.config.js /.gitattributes: -------------------------------------------------------------------------------- 1 | macros/examples/css/generated.rs linguist-generated 2 | test-project/** linguist-generated 3 | -------------------------------------------------------------------------------- /.github/workflows/audit-nightly.yml: -------------------------------------------------------------------------------- 1 | name: Security audit - nightly 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | jobs: 8 | security_audit: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions-rs/audit-check@v1 13 | with: 14 | token: ${{ secrets.GITHUB_TOKEN }} 15 | -------------------------------------------------------------------------------- /.github/workflows/audit-on-push.yml: -------------------------------------------------------------------------------- 1 | name: Security audit - on push 2 | 3 | on: 4 | push: 5 | paths: 6 | - "**/Cargo.toml" 7 | - "**/Cargo.lock" 8 | 9 | jobs: 10 | security_audit: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions-rs/audit-check@v1 15 | with: 16 | token: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous integration 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | CRATE_NAME: tailwindcss-to-rust 7 | GITHUB_TOKEN: ${{ github.token }} 8 | RUST_BACKTRACE: 1 9 | 10 | jobs: 11 | test: 12 | name: Test - ${{ matrix.platform.os_name }} with rust ${{ matrix.toolchain }} 13 | runs-on: ${{ matrix.platform.os }} 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | platform: 18 | - os_name: Linux 19 | os: ubuntu-latest 20 | target: x86_64-unknown-linux-gnu 21 | - os_name: macOS 22 | os: macOS-latest 23 | target: x86_64-apple-darwin 24 | - os_name: Windows 25 | os: windows-latest 26 | target: x86_64-pc-windows-msvc 27 | toolchain: 28 | - stable 29 | - beta 30 | - nightly 31 | steps: 32 | - uses: actions/checkout@v2 33 | - name: Cache cargo & target directories 34 | uses: Swatinem/rust-cache@v2 35 | - name: Install toolchain 36 | uses: actions-rs/toolchain@v1 37 | with: 38 | profile: default 39 | toolchain: ${{ matrix.toolchain }} 40 | override: true 41 | - name: Run cargo check 42 | uses: actions-rs/cargo@v1 43 | with: 44 | command: check 45 | args: --target=${{ matrix.platform.target }} 46 | - name: Run install-dev-tools.sh 47 | shell: bash 48 | run: | 49 | set -e 50 | mkdir $HOME/bin 51 | ./dev/bin/install-dev-tools.sh 52 | - name: Add $HOME/bin to $PATH 53 | shell: bash 54 | run: | 55 | echo "$HOME/bin" >> $GITHUB_PATH 56 | - name: Run cargo test 57 | uses: actions-rs/cargo@v1 58 | with: 59 | command: test 60 | args: --target=${{ matrix.platform.target }} --workspace 61 | 62 | # Copied from https://github.com/urbica/martin/blob/master/.github/workflows/ci.yml 63 | release: 64 | name: Release - ${{ matrix.platform.os_name }} 65 | if: startsWith( github.ref, 'refs/tags/tailwindcss-to-rust-v' ) 66 | needs: [test] 67 | strategy: 68 | matrix: 69 | platform: 70 | - os_name: Linux-x86_64 71 | os: ubuntu-20.04 72 | target: x86_64-unknown-linux-musl 73 | bin: tailwindcss-to-rust 74 | name: tailwindcss-to-rust-Linux-x86_64-musl.tar.gz 75 | cross: true 76 | - os_name: Windows 77 | os: windows-latest 78 | target: x86_64-pc-windows-msvc 79 | bin: tailwindcss-to-rust.exe 80 | name: tailwindcss-to-rust-Windows-x86_64.zip 81 | cross: false 82 | - os_name: macOS-x86_64 83 | os: macOS-latest 84 | target: x86_64-apple-darwin 85 | bin: tailwindcss-to-rust 86 | name: tailwindcss-to-rust-Darwin-x86_64.tar.gz 87 | cross: false 88 | - os_name: macOS-aarch64 89 | os: macOS-latest 90 | target: aarch64-apple-darwin 91 | bin: tailwindcss-to-rust 92 | name: tailwindcss-to-rust-Darwin-aarch64.tar.gz 93 | cross: false 94 | runs-on: ${{ matrix.platform.os }} 95 | steps: 96 | - name: Checkout 97 | uses: actions/checkout@v2 98 | - name: Install stable toolchain 99 | uses: actions-rs/toolchain@v1 100 | with: 101 | profile: minimal 102 | toolchain: stable 103 | override: true 104 | target: ${{ matrix.platform.target }} 105 | - name: Build binary 106 | uses: actions-rs/cargo@v1 107 | with: 108 | command: build 109 | args: --package tailwindcss-to-rust --release --target ${{ matrix.platform.target }} 110 | - name: Package as archive 111 | shell: bash 112 | run: | 113 | if [[ "${{ matrix.platform.cross }}" == "false" ]]; then 114 | # strip doesn't work with non-native binaries on Linux, AFAICT. 115 | strip target/${{ matrix.platform.target }}/release/${{ matrix.platform.bin }} 116 | fi 117 | cd target/${{ matrix.platform.target }}/release 118 | if [[ "${{ matrix.platform.os }}" == "windows-latest" ]]; then 119 | 7z a ../../../${{ matrix.platform.name }} ${{ matrix.platform.bin }} 120 | else 121 | tar czvf ../../../${{ matrix.platform.name }} ${{ matrix.platform.bin }} 122 | fi 123 | cd - 124 | - name: Generate SHA-256 125 | if: matrix.platform.os == 'macOS-latest' 126 | run: shasum -a 256 ${{ matrix.platform.name }} 127 | - name: Publish GitHub release 128 | uses: softprops/action-gh-release@v1 129 | with: 130 | draft: true 131 | files: "tailwindcss-to-rust*" 132 | body_path: generator/Changes.md 133 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Linting 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | CRATE_NAME: precious 7 | GITHUB_TOKEN: ${{ github.token }} 8 | RUST_BACKTRACE: 1 9 | 10 | jobs: 11 | test: 12 | name: Check that code is lint clean using precious 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-node@v2 17 | - name: Cache cargo & target directories 18 | uses: Swatinem/rust-cache@v2 19 | - name: Install toolchain 20 | uses: actions-rs/toolchain@v1 21 | with: 22 | profile: default 23 | toolchain: stable 24 | override: true 25 | - name: Configure Git 26 | run: | 27 | git config --global user.email "jdoe@example.com" 28 | git config --global user.name "J. Doe" 29 | - name: Run install-dev-tools.sh 30 | run: | 31 | set -e 32 | mkdir $HOME/bin 33 | ./dev/bin/install-dev-tools.sh 34 | - name: Run precious 35 | run: | 36 | PATH=$PATH:$HOME/bin precious lint -a 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.log 2 | target/ 3 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.20" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anyhow" 16 | version = "1.0.69" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" 19 | 20 | [[package]] 21 | name = "atty" 22 | version = "0.2.14" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 25 | dependencies = [ 26 | "hermit-abi", 27 | "libc", 28 | "winapi", 29 | ] 30 | 31 | [[package]] 32 | name = "autocfg" 33 | version = "1.1.0" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 36 | 37 | [[package]] 38 | name = "bitflags" 39 | version = "1.3.2" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 42 | 43 | [[package]] 44 | name = "cfg-if" 45 | version = "1.0.0" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 48 | 49 | [[package]] 50 | name = "clap" 51 | version = "3.2.23" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" 54 | dependencies = [ 55 | "atty", 56 | "bitflags", 57 | "clap_derive", 58 | "clap_lex", 59 | "indexmap", 60 | "once_cell", 61 | "strsim", 62 | "termcolor", 63 | "textwrap", 64 | ] 65 | 66 | [[package]] 67 | name = "clap_derive" 68 | version = "3.2.18" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" 71 | dependencies = [ 72 | "heck", 73 | "proc-macro-error", 74 | "proc-macro2", 75 | "quote", 76 | "syn", 77 | ] 78 | 79 | [[package]] 80 | name = "clap_lex" 81 | version = "0.2.4" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" 84 | dependencies = [ 85 | "os_str_bytes", 86 | ] 87 | 88 | [[package]] 89 | name = "either" 90 | version = "1.8.1" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" 93 | 94 | [[package]] 95 | name = "fastrand" 96 | version = "1.9.0" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" 99 | dependencies = [ 100 | "instant", 101 | ] 102 | 103 | [[package]] 104 | name = "hashbrown" 105 | version = "0.12.3" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 108 | 109 | [[package]] 110 | name = "heck" 111 | version = "0.4.1" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 114 | 115 | [[package]] 116 | name = "hermit-abi" 117 | version = "0.1.19" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 120 | dependencies = [ 121 | "libc", 122 | ] 123 | 124 | [[package]] 125 | name = "home" 126 | version = "0.5.4" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "747309b4b440c06d57b0b25f2aee03ee9b5e5397d288c60e21fc709bb98a7408" 129 | dependencies = [ 130 | "winapi", 131 | ] 132 | 133 | [[package]] 134 | name = "indexmap" 135 | version = "1.9.2" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" 138 | dependencies = [ 139 | "autocfg", 140 | "hashbrown", 141 | ] 142 | 143 | [[package]] 144 | name = "instant" 145 | version = "0.1.12" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 148 | dependencies = [ 149 | "cfg-if", 150 | ] 151 | 152 | [[package]] 153 | name = "integration" 154 | version = "0.1.0" 155 | dependencies = [ 156 | "anyhow", 157 | "home", 158 | ] 159 | 160 | [[package]] 161 | name = "itertools" 162 | version = "0.10.5" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 165 | dependencies = [ 166 | "either", 167 | ] 168 | 169 | [[package]] 170 | name = "itoa" 171 | version = "1.0.5" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" 174 | 175 | [[package]] 176 | name = "libc" 177 | version = "0.2.139" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" 180 | 181 | [[package]] 182 | name = "memchr" 183 | version = "2.5.0" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 186 | 187 | [[package]] 188 | name = "once_cell" 189 | version = "1.17.1" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" 192 | 193 | [[package]] 194 | name = "os_str_bytes" 195 | version = "6.4.1" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" 198 | 199 | [[package]] 200 | name = "proc-macro-error" 201 | version = "1.0.4" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 204 | dependencies = [ 205 | "proc-macro-error-attr", 206 | "proc-macro2", 207 | "quote", 208 | "syn", 209 | "version_check", 210 | ] 211 | 212 | [[package]] 213 | name = "proc-macro-error-attr" 214 | version = "1.0.4" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 217 | dependencies = [ 218 | "proc-macro2", 219 | "quote", 220 | "version_check", 221 | ] 222 | 223 | [[package]] 224 | name = "proc-macro2" 225 | version = "1.0.51" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" 228 | dependencies = [ 229 | "unicode-ident", 230 | ] 231 | 232 | [[package]] 233 | name = "quote" 234 | version = "1.0.23" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" 237 | dependencies = [ 238 | "proc-macro2", 239 | ] 240 | 241 | [[package]] 242 | name = "redox_syscall" 243 | version = "0.2.16" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 246 | dependencies = [ 247 | "bitflags", 248 | ] 249 | 250 | [[package]] 251 | name = "regex" 252 | version = "1.7.1" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" 255 | dependencies = [ 256 | "aho-corasick", 257 | "memchr", 258 | "regex-syntax", 259 | ] 260 | 261 | [[package]] 262 | name = "regex-syntax" 263 | version = "0.6.28" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" 266 | 267 | [[package]] 268 | name = "remove_dir_all" 269 | version = "0.5.3" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 272 | dependencies = [ 273 | "winapi", 274 | ] 275 | 276 | [[package]] 277 | name = "ryu" 278 | version = "1.0.12" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" 281 | 282 | [[package]] 283 | name = "serde" 284 | version = "1.0.152" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" 287 | dependencies = [ 288 | "serde_derive", 289 | ] 290 | 291 | [[package]] 292 | name = "serde_derive" 293 | version = "1.0.152" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" 296 | dependencies = [ 297 | "proc-macro2", 298 | "quote", 299 | "syn", 300 | ] 301 | 302 | [[package]] 303 | name = "serde_json" 304 | version = "1.0.93" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76" 307 | dependencies = [ 308 | "itoa", 309 | "ryu", 310 | "serde", 311 | ] 312 | 313 | [[package]] 314 | name = "strsim" 315 | version = "0.10.0" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 318 | 319 | [[package]] 320 | name = "syn" 321 | version = "1.0.107" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" 324 | dependencies = [ 325 | "proc-macro2", 326 | "quote", 327 | "unicode-ident", 328 | ] 329 | 330 | [[package]] 331 | name = "tailwindcss-to-rust" 332 | version = "0.3.2" 333 | dependencies = [ 334 | "clap", 335 | "itertools", 336 | "regex", 337 | "serde", 338 | "tempfile", 339 | "tinytemplate", 340 | ] 341 | 342 | [[package]] 343 | name = "tailwindcss-to-rust-macros" 344 | version = "0.1.3" 345 | 346 | [[package]] 347 | name = "tempfile" 348 | version = "3.3.0" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" 351 | dependencies = [ 352 | "cfg-if", 353 | "fastrand", 354 | "libc", 355 | "redox_syscall", 356 | "remove_dir_all", 357 | "winapi", 358 | ] 359 | 360 | [[package]] 361 | name = "termcolor" 362 | version = "1.2.0" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" 365 | dependencies = [ 366 | "winapi-util", 367 | ] 368 | 369 | [[package]] 370 | name = "textwrap" 371 | version = "0.16.0" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" 374 | 375 | [[package]] 376 | name = "tinytemplate" 377 | version = "1.2.1" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" 380 | dependencies = [ 381 | "serde", 382 | "serde_json", 383 | ] 384 | 385 | [[package]] 386 | name = "unicode-ident" 387 | version = "1.0.6" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" 390 | 391 | [[package]] 392 | name = "version_check" 393 | version = "0.9.4" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 396 | 397 | [[package]] 398 | name = "winapi" 399 | version = "0.3.9" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 402 | dependencies = [ 403 | "winapi-i686-pc-windows-gnu", 404 | "winapi-x86_64-pc-windows-gnu", 405 | ] 406 | 407 | [[package]] 408 | name = "winapi-i686-pc-windows-gnu" 409 | version = "0.4.0" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 412 | 413 | [[package]] 414 | name = "winapi-util" 415 | version = "0.1.5" 416 | source = "registry+https://github.com/rust-lang/crates.io-index" 417 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 418 | dependencies = [ 419 | "winapi", 420 | ] 421 | 422 | [[package]] 423 | name = "winapi-x86_64-pc-windows-gnu" 424 | version = "0.4.0" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 427 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ "generator", "integration", "macros" ] 3 | exclude = [ "test-project" ] 4 | 5 | [workspace.metadata.release] 6 | allow-branch = ["master"] 7 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This repo contains a [tool to generate Rust code from Tailwind CSS](generator) 2 | and [a set of macros for working with that generated code](macros). 3 | 4 | See the docs for those crates for more details. 5 | 6 | - [tailwindcss-to-rust](https://crates.io/crates/tailwindcss-to-rust) 7 | - [tailwindcss-to-rust-macros](https://crates.io/crates/tailwindcss-to-rust-macros) 8 | -------------------------------------------------------------------------------- /dev/bin/get-categories.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | use v5.32; 4 | 5 | use strict; 6 | use warnings; 7 | 8 | use LWP::UserAgent; 9 | use Mojo::DOM; 10 | use Time::HiRes qw( sleep ); 11 | 12 | sub main { 13 | my %links = get_links(); 14 | 15 | my %classes; 16 | for my $cat ( sort keys %links ) { 17 | for my $url ( $links{$cat}->@* ) { 18 | say $url; 19 | my $dom = Mojo::DOM->new( get($url) ); 20 | my @rows = $dom->find('table')->[0]->children('tbody')->[0]->children('tr')->@*; 21 | for my $row (@rows ) { 22 | my $td = $row->children('td')->[0]; 23 | push $classes{$cat}->@*, $td->text; 24 | } 25 | } 26 | } 27 | 28 | for my $cat (sort keys %classes) { 29 | for my $c (sort $classes{$cat}->@*) { 30 | say qq{"$c" => Some("$cat"),}; 31 | } 32 | } 33 | } 34 | 35 | sub get_links { 36 | my $dom = Mojo::DOM->new( get( make_url('/docs/installation') ) ); 37 | my $nav = $dom->find("#nav")->[0] // die "Cannot find #nav"; 38 | 39 | my %skip = map { $_ => 1 } ( 40 | 'Getting Started', 'Core Concepts', 'Customization', 41 | 'Base Styles', 'Official Plugins', 42 | ); 43 | 44 | my %links; 45 | for my $h5 ( $nav->find('h5')->@* ) { 46 | my $title = $h5->text; 47 | next if $skip{$title}; 48 | 49 | my $category = clean_category($title); 50 | my $next = $h5->following('ul')->[0]; 51 | for my $li ( $next->children('li')->@* ) { 52 | my $link = $li->children->[0]; 53 | 54 | $links{$category} //= []; 55 | push $links{$category}->@*, make_url( $link->attr('href') ); 56 | } 57 | } 58 | return %links; 59 | } 60 | 61 | sub get { 62 | my $url = shift; 63 | 64 | my $ua 65 | = LWP::UserAgent->new( agent => 66 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36' 67 | ); 68 | my $resp = $ua->get($url); 69 | 70 | my $content = $resp->decoded_content; 71 | return $content if $resp->is_success; 72 | 73 | my $msg = "Got an error from $url: " . $resp->status_line . "\n"; 74 | if ($content) { 75 | $msg .= $content; 76 | } 77 | die $msg; 78 | } 79 | 80 | my $base = 'https://tailwindcss.com'; 81 | 82 | sub make_url { 83 | $base . shift; 84 | } 85 | 86 | my %short = ( 87 | 'Transitions & Animation' => 'animation', 88 | 'Flexbox & Grid' => 'flex_and_grid', 89 | ); 90 | 91 | sub clean_category { 92 | my $name = shift; 93 | 94 | return $short{$name} if $short{$name}; 95 | return lc $name; 96 | } 97 | 98 | main(); 99 | -------------------------------------------------------------------------------- /dev/bin/install-dev-tools.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | function run () { 6 | echo $1 7 | eval $1 8 | } 9 | 10 | function install_tools () { 11 | curl --silent --location \ 12 | https://raw.githubusercontent.com/houseabsolute/ubi/master/bootstrap/bootstrap-ubi.sh | 13 | sh 14 | run "rustup component add clippy" 15 | run "ubi --project houseabsolute/precious --in ~/bin" 16 | run "ubi --project houseabsolute/omegasort --in ~/bin" 17 | run "ubi --project tailwindlabs/tailwindcss --in ~/bin" 18 | 19 | cmd="npm install --global prettier" 20 | if [ -n "$(which sudo)" ]; then 21 | cmd="sudo $cmd" 22 | fi 23 | run "$cmd" 24 | } 25 | 26 | if [ "$1" == "-v" ]; then 27 | set -x 28 | fi 29 | 30 | mkdir -p $HOME/bin 31 | 32 | set +e 33 | echo ":$PATH:" | grep --extended-regexp ":$HOME/bin:" >& /dev/null 34 | if [ "$?" -eq "0" ]; then 35 | path_has_home_bin=1 36 | fi 37 | set -e 38 | 39 | if [ -z "$path_has_home_bin" ]; then 40 | PATH=$HOME/bin:$PATH 41 | fi 42 | 43 | install_tools 44 | 45 | echo "Tools were installed into $HOME/bin." 46 | if [ -z "$path_has_home_bin" ]; then 47 | echo "You should add $HOME/bin to your PATH." 48 | fi 49 | 50 | exit 0 51 | -------------------------------------------------------------------------------- /generator/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tailwindcss-to-rust" 3 | version = "0.3.2" 4 | authors = ["Dave Rolsky "] 5 | description = "Generate Rust code from your compiled tailwind CSS" 6 | repository = "https://github.com/houseabsolute/tailwindcss-to-rust" 7 | readme = "README.md" 8 | license = "Apache-2.0 OR MIT" 9 | edition = "2021" 10 | 11 | [dependencies] 12 | clap = { version = "3.2.23", features = ["derive"] } 13 | itertools = "0.10.5" 14 | regex = "1.7.1" 15 | serde = { version = "1.0.152", features = ["derive"] } 16 | tempfile = "3.3.0" 17 | tinytemplate = "1.2.1" 18 | 19 | [features] 20 | -------------------------------------------------------------------------------- /generator/Changes.md: -------------------------------------------------------------------------------- 1 | ## v0.3.2 - 2023-02-19 2 | 3 | - Fixed the generator to run on Windows without a stack overflow. 4 | 5 | ## v0.3.1 - 2023-02-18 6 | 7 | - Fixes for docs and release tooling. 8 | 9 | ## v0.3.0 - 2023-02-18 10 | 11 | - The generated code now groups CSS classes into modules instead of 12 | structs. This prevents stack overflows that happened when the structs were 13 | put on the stack. Thanks to @mdochdev for identifying the issue and 14 | suggesting this fix. GH #4. 15 | 16 | - Updated the class categories for the latest TailwindCSS version, 3.2.7. 17 | 18 | ## v0.2.0 - 2023-02-05 19 | 20 | - Updated the class categories for the latest TailwindCSS version, 3.2.4. 21 | 22 | - Updated the docs to make it clearer how to configure `tailwind.config.js` 23 | depending on whether or not you're using the macros, what templating system 24 | you're using, etc. 25 | 26 | ## v0.1.4 - 2022-04-24 27 | 28 | - Added a new `--tailwindcss` argument. This can be used to provide the path 29 | to the `tailwindcss` executable. 30 | 31 | ## v0.1.3 - 2022-02-17 32 | 33 | - Documentation fixes. 34 | 35 | ## v0.1.2 - 2022-02-14 36 | 37 | - Fixed the handling of CSS class names with periods and forward slashes. The 38 | generated Rust code included an escape for these names, so they'd end up as 39 | things like `w-0\.5` or `w-3\/5` in your HTML, which is wrong. They should 40 | not have a backslash escape in the generated HTML. Note that this also means 41 | that the tailwind extractor code in the generator's `README.md` was wrong as 42 | well. It has also been fixed. 43 | 44 | ## v0.1.1 - 2022-02-08 45 | 46 | - Fixed the repository metadata for the crate. Thanks to @overlisted on the 47 | Dioxus Discord for pointing this out. 48 | 49 | ## v0.1.0 - 2022-02-08 50 | 51 | - First release upon an unsuspecting world. 52 | -------------------------------------------------------------------------------- /generator/README.md: -------------------------------------------------------------------------------- 1 | The `tailwindcss-to-rust` CLI tool generates Rust code that allows you to 2 | refer to Tailwind classes from your Rust code. This means that any attempt to 3 | use a nonexistent class will lead to a compile-time error, and you can use 4 | code completion to list available classes. 5 | 6 | **This tool has been tested with version 3.2.x of Tailwind.** 7 | 8 | The generated code allows you to use Tailwind CSS classes in your Rust 9 | frontend code with compile-time checking of names and code completion for 10 | class names. These classes are grouped together based on the heading in the 11 | Tailwind docs. It also generates code for the full list of Tailwind modifiers 12 | like `lg`, `hover`, etc. 13 | 14 | [**Check out the tailwindcss-to-rust-macros 15 | crate**](https://crates.io/crates/tailwindcss-to-rust-macros) for the most 16 | ergonomic way to use the code generated by this tool. 17 | 18 | So instead of this: 19 | 20 | ```rust,ignore 21 | let class = "pt-4 pb-2 text-whit"; 22 | ``` 23 | 24 | You can write this: 25 | 26 | ```rust,ignore 27 | let class = C![C::spc::pt_4 C::pb_2 C::typ::text_white]; 28 | ``` 29 | 30 | Note that the typo in the first example, **"text-whit"** (missing the "e") 31 | would become a compile-time error if you wrote `C::typ::text_whit`. 32 | 33 | Here's a quick start recipe: 34 | 35 | 1. Install this tool by running: 36 | 37 | ``` 38 | cargo install tailwindcss-to-rust 39 | ``` 40 | 41 | 2. [Install the `tailwindcss` CLI 42 | tool](https://tailwindcss.com/docs/installation). You can install it with 43 | `npm` or `npx`, or you can [download a standalone binary from the 44 | tailwindcss repo](https://github.com/tailwindlabs/tailwindcss/releases). 45 | 46 | 3. Create a `tailwind.config.js` file with the tool by running: 47 | 48 | ```sh 49 | tailwindcss init 50 | ``` 51 | 52 | 4. Edit this file however you like to add plugins or customize the generated 53 | CSS. 54 | 55 | 5. Create a CSS input file for Tailwind. For the purposes of this example we 56 | will assume that it's located at `css/tailwind.css`. The standard file 57 | looks like this: 58 | 59 | ```css 60 | @tailwind base; 61 | @tailwind components; 62 | @tailwind utilities; 63 | ``` 64 | 65 | 6. Generate your Rust code by running: 66 | 67 | ```sh 68 | tailwindcss-to-rust \ 69 | --tailwind-config tailwind.config.js \ 70 | --input tailwind.css \ 71 | --output src/css/generated.rs \ 72 | --rustfmt 73 | ``` 74 | 75 | **The `tailwindcss` executable must be in your `PATH` when you run 76 | `tailwindcss-to-rust` or you must provide the path to the executable in the 77 | `--tailwindcss` argument.** 78 | 79 | 7. Edit your `tailwind.config.js` file to look in your Rust files for Tailwind 80 | class names: 81 | 82 | ```js 83 | /** @type {import('tailwindcss').Config} */ 84 | module.exports = { 85 | content: { 86 | files: ["index.html", "**/*.rs"], 87 | // You do need to copy this big block of code in, unfortunately. 88 | extract: { 89 | rs: (content) => { 90 | const rs_to_tw = (rs) => { 91 | if (rs.startsWith("two_")) { 92 | rs = rs.replace("two_", "2"); 93 | } 94 | return rs 95 | .replaceAll("_of_", "/") 96 | .replaceAll("_p_", ".") 97 | .replaceAll("_", "-"); 98 | }; 99 | 100 | let one_class_re = "\\bC::[a-z0-9_]+::([a-z0-9_]+)\\b"; 101 | let class_re = new RegExp(one_class_re, "g"); 102 | let one_mod_re = "\\bM::([a-z0-9_]+)\\b"; 103 | let mod_re = new RegExp(one_mod_re + ", " + one_class_re, "g"); 104 | 105 | let classes = []; 106 | let matches = [...content.matchAll(mod_re)]; 107 | if (matches.length > 0) { 108 | classes.push( 109 | ...matches.map((m) => { 110 | let pieces = m.slice(1, m.length); 111 | return pieces.map((p) => rs_to_tw(p)).join(":"); 112 | }) 113 | ); 114 | } 115 | classes.push( 116 | ...[...content.matchAll(class_re)].map((m) => { 117 | return rs_to_tw(m[1]); 118 | }) 119 | ); 120 | 121 | return classes; 122 | }, 123 | }, 124 | }, 125 | theme: { 126 | extend: {}, 127 | }, 128 | plugins: [], 129 | }; 130 | ``` 131 | 132 | **Note that you may need to customize the regexes in the 133 | `extract` function to match your templating system!** The regexes in this 134 | example will match the syntax you'd use with the 135 | [tailwindcss-to-rust-macros](https://crates.io/crates/tailwindcss-to-rust-macros) 136 | crate. 137 | 138 | For example, if you're using [askama](https://crates.io/crates/askama) 139 | without the macros then you will need to match something like this: 140 | 141 | ```html 142 |
145 | ... 146 |
147 | ``` 148 | 149 | The regexes for that would look something like this: 150 | 151 | ```js 152 | let one_class_re = "{{\\s*C::[a-z0-9_]+::([a-z0-9_]+)\\s*}}"; 153 | let class_re = new RegExp(one_class_re, "g"); 154 | let one_mod_re = "{{\\s*M::([a-z0-9_]+)\\s*}}"; 155 | let mod_re = new RegExp(one_mod_re + ":" + one_class_re, "g"); 156 | ``` 157 | 158 | 8. Hack, hack, hack ... 159 | 160 | 9. Regenerate your compiled Tailwind CSS file by running: 161 | 162 | ```sh 163 | tailwindcss --input css/tailwind.css --output css/tailwind_compiled.css` 164 | ``` 165 | 166 | 10. Make sure to import the compiled CSS in your HTML: 167 | 168 | ```html 169 | 170 | ``` 171 | 172 | In this example, I'm using [Trunk](https://trunkrs.dev/), which is a great 173 | alternative to webpack for projects that want to use Rust -> WASM without any 174 | node.js tooling. My `Trunk.toml` looks like this: 175 | 176 | ```toml 177 | [build] 178 | target = "index.html" 179 | dist = "dist" 180 | 181 | [[hooks]] 182 | stage = "build" 183 | # I'm not sure why we can't just invoke tailwindcss directly, but that doesn't 184 | # seem to work for some reason. 185 | command = "sh" 186 | command_arguments = ["-c", "tailwindcss -i css/tailwind.css -o css/tailwind_compiled.css"] 187 | ``` 188 | 189 | When I run `trunk` I have to make sure to ignore that generated file: 190 | 191 | ```sh 192 | trunk --ignore ./css/tailwind_compiled.css ... 193 | ``` 194 | 195 | The generated names consist of all the class names present in the CSS file, 196 | except names that start with a dash (`-`), names that contain pseudo-elements, 197 | like `.placeholder-opacity-100::-moz-placeholder`, and names that contain 198 | modifiers like `lg` or `hover`. Names are transformed into Rust identifiers 199 | using the following algorithm: 200 | 201 | - All backslash escapes are removed entirely, for example in `.inset-0\.5`. 202 | - All dashes (`-`) become underscores (`_`). 203 | - All periods (`.`) become `_p_`, so `.inset-2\.5` becomes `inset_2_p_5`. 204 | - All forward slashes (`/`) become `_of_`, so `.inset-2\/4` becomes 205 | `inset_2_of_4`. 206 | - If a name _starts_ with a `2`, as in `2xl`, it becomes `two_`, so the `2xl` 207 | modifier becomes `two_xl`. 208 | - The name `static` becomes `static_`. 209 | 210 | The generated code provides two modules containing all of the relevant 211 | strings. 212 | 213 | The `C` module contains a number of submodules, one for each group of classes 214 | as documented in the TailwindCSS docs. The groups are as follows: 215 | 216 | ```rust,ignore 217 | pub(crate) mod C { 218 | // Accessibility 219 | pub(crate) mod acc { ... } 220 | 221 | // Animation 222 | pub(crate) mod anim { ... } 223 | 224 | // Backgrounds 225 | pub(crate) mod bg { ... } 226 | 227 | // Borders 228 | pub(crate) mod bor { ... } 229 | 230 | // Effects 231 | pub(crate) mod eff { ... } 232 | 233 | // Filter 234 | pub(crate) mod fil { ... } 235 | 236 | // Flexbox & Grid 237 | pub(crate) mod fg { ... } 238 | 239 | // Interactivity 240 | pub(crate) mod intr { ... } 241 | 242 | // Layout 243 | pub(crate) mod lay { ... } 244 | 245 | // Sizing 246 | pub(crate) mod siz { ... } 247 | 248 | // Spacing 249 | pub(crate) mod spc { ... } 250 | 251 | // SVG 252 | pub(crate) mod svg { ... } 253 | 254 | // Tables 255 | pub(crate) mod tbl { ... } 256 | 257 | // Transforms 258 | pub(crate) mod trn { ... } 259 | 260 | // Typography 261 | pub(crate) mod typ { ... } 262 | } 263 | ``` 264 | 265 | In your code, you can refer to classes with `C::typ::text_lg` or 266 | `C::lay::flex`. If you have any custom classes, these will end in an "unknown" 267 | group available from `C::unk`. Adding a way to put these custom classes in 268 | other groups is a todo item. 269 | 270 | The modifiers have their own module, `M`, which contains one field per 271 | modifier, so it's used as `M::lg` or `M::hover`. A few modifiers which are 272 | parameterizable are not included, like `aria-*`, `data-*`, etc. 273 | 274 | The best way to understand the generated modules is to open the generated code 275 | file in your editor and look at it. 276 | 277 | Then you can import these consts in your code and use them to refer to 278 | Tailwind CSS class names with compile time checking: 279 | 280 | ```rust,ignore 281 | element.set_class(C::lay::aspect_auto); 282 | ``` 283 | -------------------------------------------------------------------------------- /git/hooks/pre-commit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | status=0 4 | 5 | PRECIOUS=$(which precious) 6 | if [[ -z $PRECIOUS ]]; then 7 | PRECIOUS=./bin/precious 8 | fi 9 | 10 | "$PRECIOUS" lint -s 11 | if (( $? != 0 )); then 12 | status+=1 13 | fi 14 | 15 | exit $status 16 | -------------------------------------------------------------------------------- /git/setup.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Cwd qw( abs_path ); 7 | 8 | symlink_hook('pre-commit'); 9 | 10 | sub symlink_hook { 11 | my $hook = shift; 12 | 13 | my $dot = ".git/hooks/$hook"; 14 | my $file = "git/hooks/$hook.sh"; 15 | my $link = "../../$file"; 16 | 17 | if ( -e $dot ) { 18 | if ( -l $dot ) { 19 | return if readlink $dot eq $link; 20 | } 21 | warn "You already have a hook at $dot!\n"; 22 | return; 23 | } 24 | 25 | symlink $link, $dot; 26 | } 27 | -------------------------------------------------------------------------------- /integration/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "integration" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | anyhow = "1.0.69" 8 | home = "0.5.4" 9 | 10 | [[test]] 11 | name = "integration_tests" 12 | path = "tests/lib.rs" 13 | harness = true 14 | -------------------------------------------------------------------------------- /integration/tests/lib.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use std::{ 3 | fs::read_to_string, 4 | path::PathBuf, 5 | process::{Command, Output}, 6 | str, 7 | }; 8 | 9 | #[test] 10 | fn regen() -> Result<()> { 11 | // The commands run below should match the ones in test-project/regen.sh. 12 | 13 | let home = home::home_dir() 14 | .expect("should be able to determine a home dir") 15 | .display() 16 | .to_string(); 17 | let tailwindcss_exe = PathBuf::from_iter([&home, "bin", "tailwindcss"]) 18 | .display() 19 | .to_string(); 20 | let test_project_dir = PathBuf::from_iter(["..", "test-project"]) 21 | .display() 22 | .to_string(); 23 | 24 | run_command(&["cargo", "build", "-p", "tailwindcss-to-rust"], "..")?; 25 | 26 | let tailwindcss_to_rust_exe = 27 | PathBuf::from_iter(["..", "target", "debug", "tailwindcss-to-rust"]) 28 | .display() 29 | .to_string(); 30 | run_command( 31 | &[ 32 | &tailwindcss_to_rust_exe, 33 | "--tailwind-config", 34 | "tailwind.config.js", 35 | "--input", 36 | "./css/tailwind.css", 37 | "--output", 38 | "./src/generated.rs", 39 | "--tailwindcss", 40 | &tailwindcss_exe, 41 | "--rustfmt", 42 | ], 43 | &test_project_dir, 44 | )?; 45 | 46 | run_command( 47 | &[ 48 | &tailwindcss_exe, 49 | "--input", 50 | "./css/tailwind.css", 51 | "--output", 52 | "./assets/tailwind_compiled.css", 53 | ], 54 | &test_project_dir, 55 | )?; 56 | 57 | let run = run_command(&["cargo", "run"], &test_project_dir)?; 58 | 59 | let expect = r#" 60 | bg-rose-500 61 | hover 62 | hover:bg-blue-50 text-white 63 | "#; 64 | assert_eq!(str::from_utf8(&run.stdout)?.trim(), expect.trim()); 65 | 66 | let css = read_to_string("../test-project/assets/tailwind_compiled.css")?; 67 | for expect in &[ 68 | ".bg-blue-50 {", 69 | ".bg-rose-500 {", 70 | ".text-white {", 71 | ".hover\\:bg-blue-50:hover {", 72 | ] { 73 | assert!(css.contains(expect)); 74 | } 75 | 76 | Ok(()) 77 | } 78 | 79 | fn run_command(cmd: &[&str], cwd: &str) -> Result { 80 | let cstr = cmd.join(" "); 81 | let output = Command::new(cmd[0]) 82 | .args(&cmd[1..]) 83 | .current_dir(cwd) 84 | .output() 85 | .context(format!("running [{cstr}]"))?; 86 | check_output(&cstr, &output); 87 | Ok(output) 88 | } 89 | 90 | fn check_output(cstr: &str, output: &Output) { 91 | if !output.status.success() { 92 | println!("{cstr} failed:"); 93 | if let Ok(stdout) = str::from_utf8(&output.stdout) { 94 | if !stdout.is_empty() { 95 | print!("stdout:\n{stdout}"); 96 | } 97 | } 98 | if let Ok(stderr) = str::from_utf8(&output.stderr) { 99 | if !stderr.is_empty() { 100 | print!("stderr:\n{stderr}"); 101 | } 102 | } 103 | } 104 | assert!(output.status.success()); 105 | } 106 | -------------------------------------------------------------------------------- /macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tailwindcss-to-rust-macros" 3 | version = "0.1.3" 4 | authors = ["Dave Rolsky "] 5 | description = "Helpers macros for using the Rust code generated by tailwindcss-to-rust" 6 | repository = "https://github.com/houseabsolute/tailwindcss-to-rust" 7 | readme = "README.md" 8 | license = "Apache-2.0 OR MIT" 9 | edition = "2021" 10 | -------------------------------------------------------------------------------- /macros/Changes.md: -------------------------------------------------------------------------------- 1 | ## v0.1.3 - 2023-02-18 2 | 3 | - Updated the docs to reflect the changes in version 0.3.0 of the 4 | tailwindcss-to-rust code generator. 5 | 6 | ## v0.1.2 - 2022-02-17 7 | 8 | - Documentation and example fixes. 9 | 10 | ## v0.1.1 - 2022-02-08 11 | 12 | - Fixed the repository metadata for this crate. Thanks to @overlisted on the 13 | Dioxus Discord for pointing this out. 14 | 15 | ## v0.1.0 - 2022-02-08 16 | 17 | - First release upon an unsuspecting world. 18 | -------------------------------------------------------------------------------- /macros/README.md: -------------------------------------------------------------------------------- 1 | Helper macros for using the code generated by 2 | [tailwindcss-to-rust](https://crates.io/crates/tailwindcss-to-rust). 3 | 4 | The generated code provides a number of static structs where each field is 5 | a class name or modifier (like "lg" or "hover"). In typical use, you need 6 | to combine multiple names and modifiers into a single string to be set as 7 | an element's `class` attribute. This crate provides two macros to make 8 | using this a bit more ergonomic. 9 | 10 | A [Dioxus](https://dioxuslabs.com/) example: 11 | 12 | ```rust,ignore 13 | // Note that you have to write this css module to tie it all together. 14 | use css::*; 15 | use dioxus_prelude::*; 16 | 17 | fn SomeComponent(cx: Scope) -> Element { 18 | cx.render(rsx! { 19 | div { 20 | // "grid-cols-3 md:grid-cols-6 lg:grid-cols-12" 21 | class: DC![ 22 | C::fg::grid-cols-3, 23 | M![M::md, C::fg::grid-cols-6], 24 | M![M::lg, C::fg::grid-cols-12] 25 | ], 26 | div { 27 | // "text-lg text-white" 28 | class: DC![C::typ::text_lg, C::typ::text_white], 29 | } 30 | } 31 | }) 32 | } 33 | ``` 34 | -------------------------------------------------------------------------------- /macros/examples/css/mod.rs: -------------------------------------------------------------------------------- 1 | // This is the code generated by the tailwindcss-to-rust tool. 2 | pub(crate) mod generated; 3 | 4 | // You need to re-export the generated structs, the macros, and the 5 | // `ToOptionVecstring` trait. 6 | pub(crate) use generated::{C, M}; 7 | // If you're using Dioxus you'll also want to re-export the `DC` macro. 8 | pub(crate) use tailwindcss_to_rust_macros::{ToOptionVecString, C, M}; 9 | -------------------------------------------------------------------------------- /macros/examples/main.rs: -------------------------------------------------------------------------------- 1 | mod css; 2 | 3 | use css::*; 4 | 5 | fn main() { 6 | println!("{}", C![C::typ::text_white, M![M::lg, C::typ::text_lg]]); 7 | } 8 | -------------------------------------------------------------------------------- /macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | //! 3 | //! It's convenient to have a single module in your codebase that exports the 4 | //! macros along with the generated structs. 5 | //! 6 | //! Let's assume that module will live at `src/css/mod.rs` and that the 7 | //! code generated by `tailwindcss-to-rust` lives at `src/css/generated.rs`. The 8 | //! contents of `mod.rs` will look like this: 9 | //! 10 | //! ```rust,ignore 11 | #![doc = include_str!("../examples/css/mod.rs")] 12 | //! ``` 13 | //! 14 | //! See [the `examples` directory in the git 15 | //! repo](https://github.com/houseabsolute/tailwindcss-to-rust/tree/master/macros/examples) 16 | //! for all of the above example code. 17 | 18 | pub mod to_option_vec_string; 19 | pub use to_option_vec_string::ToOptionVecString; 20 | 21 | /// Takes one or more class names and returns a single space-separated string. 22 | /// 23 | /// This macro provides a bit of sugar. 24 | /// 25 | /// It frees you from having to write something like this: 26 | /// 27 | /// ```rust,ignore 28 | /// let classes = [C.lay.flex, C.fg.flex_col].join(" "); 29 | /// ``` 30 | /// 31 | /// It also offers a lot of flexibility in what types it accepts, so you can 32 | /// use any of the following as arguments to `C!`: 33 | /// 34 | /// * `&str` 35 | /// * `String` 36 | /// * `&String` 37 | /// * `Option` and `&Option` where `T` is any of the above. 38 | /// * `Vec`, `&Vec`, and `&[T]` where `T` is any of the above. 39 | #[macro_export] 40 | macro_rules! C { 41 | ( $($class:expr $(,)?)+ ) => { 42 | { 43 | // [ 44 | // $( 45 | // $class.to_option_vec_string(), 46 | // )* 47 | // ].into_iter().filter_map(Option::is_some).flatten().join(" ") 48 | let mut all_classes = vec![]; 49 | $( 50 | $crate::_push_all_strings(&mut all_classes, $class.to_option_vec_string()); 51 | )* 52 | all_classes.join(" ") 53 | } 54 | }; 55 | } 56 | 57 | /// Variant of the [`C!`] macro for use with Dioxus `class` attributes. 58 | /// 59 | /// This works exactly like [`C!`] but it is designed to work with Dioxus's 60 | /// attributes. 61 | /// 62 | /// If you want to import this as `C!` just write this: 63 | /// 64 | /// ```rust,ignore 65 | /// use tailwindcss_to_rust_macros::DC as C; 66 | /// 67 | /// div { 68 | /// class: DC![C.typ.text_lg], 69 | /// ... 70 | /// } 71 | /// ``` 72 | #[macro_export] 73 | macro_rules! DC { 74 | ( $($class:expr $(,)?)+ ) => { 75 | { 76 | let mut all_classes = vec![]; 77 | $( 78 | $crate::_push_all_strings(&mut all_classes, $class.to_option_vec_string()); 79 | )* 80 | format_args!("{}", all_classes.join(" ")) 81 | } 82 | }; 83 | } 84 | 85 | /// Takes one or more tailwind modifier names and a class name, returning a single colon-separated string. 86 | /// 87 | /// This works exactly like [`C!`] but it expects one or more modifier names 88 | /// like "lg" or "hover", followed by a single class name. 89 | /// 90 | /// ```rust,ignore 91 | /// let classes = [ 92 | /// C.flex_and_grid.grid_cols_3, 93 | /// M![M.lg, C.fg.grid_cols_6], 94 | /// ].join(" "); 95 | /// // classes is "grid-cols-3 lg:grid-cols-6" 96 | /// ``` 97 | #[macro_export] 98 | macro_rules! M { 99 | ( $($modifier:expr $(,)?)* ) => { 100 | { 101 | let mut all_modifiers = vec![]; 102 | $( 103 | $crate::_push_all_strings(&mut all_modifiers, $modifier.to_option_vec_string()); 104 | )* 105 | all_modifiers.join(":") 106 | } 107 | }; 108 | } 109 | 110 | /// This is for use by the macros. Please don't use it yourself. 111 | pub fn _push_all_strings(all_strings: &mut Vec, classes: Option>) { 112 | if let Some(classes) = classes { 113 | all_strings.append( 114 | &mut classes 115 | .into_iter() 116 | .filter(|c| !c.is_empty()) 117 | .collect::>(), 118 | ); 119 | } 120 | } 121 | 122 | #[cfg(test)] 123 | mod tests { 124 | use super::*; 125 | 126 | #[test] 127 | fn test() { 128 | assert_eq!(C![""], ""); 129 | assert_eq!(C!["x"], "x"); 130 | 131 | let x = "x"; 132 | let y = "y"; 133 | assert_eq!(C![x, y], "x y"); 134 | assert_eq!(C![x, y, "z"], "x y z"); 135 | 136 | assert_eq!(C![M!["md", "x"]], "md:x"); 137 | assert_eq!(C![M!["md", "hover", "x"]], "md:hover:x"); 138 | assert_eq!(C![M!["md", "x"], M!["hover", "y"]], "md:x hover:y"); 139 | assert_eq!(C![x, M!["md", y], M!["hover", "z"]], "x md:y hover:z"); 140 | 141 | let z = "z".to_string(); 142 | assert_eq!(C![x, y, z, "foo"], "x y z foo"); 143 | 144 | let z = "z".to_string(); 145 | assert_eq!(C![M![x, y, z, "foo"]], "x:y:z:foo"); 146 | } 147 | 148 | // These tests were copied from Seed (https://github.com/seed-rs/seed) and 149 | // then modified. 150 | // 151 | // Copyright 2019 DavidOConnor 152 | // 153 | // Licensed under the MIT license only. 154 | 155 | // --- Texts --- 156 | 157 | #[test] 158 | fn to_option_vec_string_ref_str() { 159 | let text: &str = "foo"; 160 | assert_eq!(C![text], "foo"); 161 | assert_eq!(M![text], "foo"); 162 | } 163 | 164 | #[test] 165 | fn to_option_vec_string_ref_str_empty() { 166 | let text: &str = ""; 167 | assert!(C![text].is_empty()); 168 | assert!(M![text].is_empty()); 169 | } 170 | 171 | #[test] 172 | fn to_option_vec_string_string() { 173 | let text = String::from("bar"); 174 | assert_eq!(C![text], "bar"); 175 | let text = String::from("bar"); 176 | assert_eq!(M![text], "bar"); 177 | } 178 | 179 | #[test] 180 | fn to_option_vec_string_ref_string() { 181 | let text = &String::from("ref_bar"); 182 | assert_eq!(C![text], "ref_bar"); 183 | let text = &String::from("ref_bar"); 184 | assert_eq!(M![text], "ref_bar"); 185 | } 186 | 187 | // --- Containers --- 188 | 189 | #[test] 190 | fn to_option_vec_string_vec() { 191 | let vec: Vec<&str> = vec!["foo_1", "foo_2"]; 192 | assert_eq!(C![vec], "foo_1 foo_2"); 193 | let vec: Vec<&str> = vec!["foo_1", "foo_2"]; 194 | assert_eq!(M![vec], "foo_1:foo_2"); 195 | } 196 | 197 | #[test] 198 | fn to_option_vec_string_ref_vec() { 199 | let vec: &Vec<&str> = &vec!["foo_1", "foo_2"]; 200 | assert_eq!(C![vec], "foo_1 foo_2"); 201 | let vec: &Vec<&str> = &vec!["foo_1", "foo_2"]; 202 | assert_eq!(M![vec], "foo_1:foo_2"); 203 | } 204 | 205 | #[test] 206 | fn to_option_vec_string_slice() { 207 | let slice: &[&str] = &["foo_1", "foo_2"]; 208 | assert_eq!(C![slice], "foo_1 foo_2"); 209 | assert_eq!(M![slice], "foo_1:foo_2"); 210 | } 211 | 212 | #[test] 213 | fn to_option_vec_string_option_some() { 214 | let option: Option<&str> = Some("foo_opt"); 215 | assert_eq!(C![option], "foo_opt"); 216 | assert_eq!(M![option], "foo_opt"); 217 | } 218 | 219 | #[test] 220 | fn to_option_vec_string_ref_option_some() { 221 | let option: &Option<&str> = &Some("foo_opt"); 222 | assert_eq!(C![option], "foo_opt"); 223 | assert_eq!(M![option], "foo_opt"); 224 | } 225 | 226 | #[test] 227 | fn to_option_vec_string_option_none() { 228 | let option: Option<&str> = None; 229 | assert!(C![option].is_empty()); 230 | assert!(M![option].is_empty()); 231 | } 232 | 233 | #[test] 234 | fn to_option_vec_string_option_vec() { 235 | let option_vec: Option> = Some(vec!["foo_1", "foo_2"]); 236 | assert_eq!(C![option_vec], "foo_1 foo_2"); 237 | let option_vec: Option> = Some(vec!["foo_1", "foo_2"]); 238 | assert_eq!(M![option_vec], "foo_1:foo_2"); 239 | } 240 | 241 | // I wrote this to help debug an issue with a Dioxus application where 242 | // similar code was leading to memory errors in the generated WASM. 243 | #[test] 244 | fn with_fmt() { 245 | struct Classes { 246 | classes: String, 247 | } 248 | 249 | impl std::fmt::Display for Classes { 250 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 251 | write!(f, "{} {}", self.classes, C!["foo", "bar" M!["md", "baz"]],) 252 | } 253 | } 254 | 255 | let classes = Classes { 256 | classes: "x y z".to_string(), 257 | }; 258 | assert_eq!(format!("{classes}"), "x y z foo bar md:baz"); 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /macros/src/to_option_vec_string.rs: -------------------------------------------------------------------------------- 1 | // This code was copied from Seed (https://github.com/seed-rs/seed) and 2 | // modified. 3 | // 4 | // Copyright 2019 DavidOConnor 5 | // 6 | // Licensed under the MIT license only. 7 | 8 | //! Contains a trait to make the macros work with many types. 9 | //! 10 | //! ```rust,ignore 11 | //! use tailwindcss_to_rust_macros::{C, M, ToOptionvecstring}; 12 | //! ``` 13 | /// You need to make sure this trait is imported by any code that wants to use 14 | /// the `C!`, `DC!`, or `M!` macros. 15 | pub trait ToOptionVecString { 16 | fn to_option_vec_string(self) -> Option>; 17 | } 18 | 19 | // ------ Implementations ------ 20 | 21 | impl ToOptionVecString for &T { 22 | fn to_option_vec_string(self) -> Option> { 23 | self.clone().to_option_vec_string() 24 | } 25 | } 26 | 27 | // --- Texts --- 28 | 29 | impl ToOptionVecString for String { 30 | fn to_option_vec_string(self) -> Option> { 31 | Some(vec![self]) 32 | } 33 | } 34 | 35 | impl ToOptionVecString for &str { 36 | fn to_option_vec_string(self) -> Option> { 37 | Some(vec![self.to_string()]) 38 | } 39 | } 40 | 41 | // --- Containers --- 42 | 43 | impl ToOptionVecString for Option { 44 | fn to_option_vec_string(self) -> Option> { 45 | self.and_then(ToOptionVecString::to_option_vec_string) 46 | } 47 | } 48 | 49 | impl ToOptionVecString for Vec { 50 | fn to_option_vec_string(self) -> Option> { 51 | let classes = self 52 | .into_iter() 53 | .filter_map(ToOptionVecString::to_option_vec_string) 54 | .flatten(); 55 | Some(classes.collect()) 56 | } 57 | } 58 | 59 | impl ToOptionVecString for &[T] { 60 | fn to_option_vec_string(self) -> Option> { 61 | let classes = self 62 | .iter() 63 | .filter_map(ToOptionVecString::to_option_vec_string) 64 | .flatten(); 65 | Some(classes.collect()) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /precious.toml: -------------------------------------------------------------------------------- 1 | exclude = [ 2 | "target", 3 | ] 4 | 5 | [commands.rustfmt] 6 | type = "both" 7 | include = "**/*.rs" 8 | cmd = [ "rustfmt", "--edition", "2021" ] 9 | lint_flags = "--check" 10 | ok_exit_codes = 0 11 | lint_failure_exit_codes = 1 12 | 13 | [commands.clippy] 14 | type = "lint" 15 | include = "**/*.rs" 16 | run_mode = "root" 17 | chdir = true 18 | cmd = [ "cargo", "clippy", "--locked", "--all-targets", "--all-features", "--", "-D", "clippy::all" ] 19 | ok_exit_codes = 0 20 | lint_failure_exit_codes = 101 21 | expect_stderr = true 22 | 23 | [commands.prettier] 24 | type = "both" 25 | include = [ "**/*.md", "**/*.js", "**/*.yml" ] 26 | cmd = "prettier" 27 | lint_flags = "--check" 28 | tidy_flags = "--write" 29 | ok_exit_codes = 0 30 | lint_failure_exit_codes = 1 31 | 32 | [commands.omegasort-gitignore] 33 | type = "both" 34 | include = "**/.gitignore" 35 | cmd = [ "omegasort", "--sort=path" ] 36 | lint_flags = "--check" 37 | tidy_flags = "--in-place" 38 | ok_exit_codes = 0 39 | lint_failure_exit_codes = 1 40 | expect_stderr = true 41 | -------------------------------------------------------------------------------- /test-project/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "tailwindcss-to-rust-macros" 7 | version = "0.1.3" 8 | 9 | [[package]] 10 | name = "test-project" 11 | version = "0.1.0" 12 | dependencies = [ 13 | "tailwindcss-to-rust-macros", 14 | ] 15 | -------------------------------------------------------------------------------- /test-project/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-project" 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 | tailwindcss-to-rust-macros = { path = "../macros" } 10 | -------------------------------------------------------------------------------- /test-project/assets/tailwind_compiled.css: -------------------------------------------------------------------------------- 1 | /* 2 | ! tailwindcss v3.2.7 | MIT License | https://tailwindcss.com 3 | */ 4 | 5 | /* 6 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) 7 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) 8 | */ 9 | 10 | *, 11 | ::before, 12 | ::after { 13 | box-sizing: border-box; 14 | /* 1 */ 15 | border-width: 0; 16 | /* 2 */ 17 | border-style: solid; 18 | /* 2 */ 19 | border-color: #e5e7eb; 20 | /* 2 */ 21 | } 22 | 23 | ::before, 24 | ::after { 25 | --tw-content: ''; 26 | } 27 | 28 | /* 29 | 1. Use a consistent sensible line-height in all browsers. 30 | 2. Prevent adjustments of font size after orientation changes in iOS. 31 | 3. Use a more readable tab size. 32 | 4. Use the user's configured `sans` font-family by default. 33 | 5. Use the user's configured `sans` font-feature-settings by default. 34 | */ 35 | 36 | html { 37 | line-height: 1.5; 38 | /* 1 */ 39 | -webkit-text-size-adjust: 100%; 40 | /* 2 */ 41 | -moz-tab-size: 4; 42 | /* 3 */ 43 | -o-tab-size: 4; 44 | tab-size: 4; 45 | /* 3 */ 46 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 47 | /* 4 */ 48 | font-feature-settings: normal; 49 | /* 5 */ 50 | } 51 | 52 | /* 53 | 1. Remove the margin in all browsers. 54 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. 55 | */ 56 | 57 | body { 58 | margin: 0; 59 | /* 1 */ 60 | line-height: inherit; 61 | /* 2 */ 62 | } 63 | 64 | /* 65 | 1. Add the correct height in Firefox. 66 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) 67 | 3. Ensure horizontal rules are visible by default. 68 | */ 69 | 70 | hr { 71 | height: 0; 72 | /* 1 */ 73 | color: inherit; 74 | /* 2 */ 75 | border-top-width: 1px; 76 | /* 3 */ 77 | } 78 | 79 | /* 80 | Add the correct text decoration in Chrome, Edge, and Safari. 81 | */ 82 | 83 | abbr:where([title]) { 84 | -webkit-text-decoration: underline dotted; 85 | text-decoration: underline dotted; 86 | } 87 | 88 | /* 89 | Remove the default font size and weight for headings. 90 | */ 91 | 92 | h1, 93 | h2, 94 | h3, 95 | h4, 96 | h5, 97 | h6 { 98 | font-size: inherit; 99 | font-weight: inherit; 100 | } 101 | 102 | /* 103 | Reset links to optimize for opt-in styling instead of opt-out. 104 | */ 105 | 106 | a { 107 | color: inherit; 108 | text-decoration: inherit; 109 | } 110 | 111 | /* 112 | Add the correct font weight in Edge and Safari. 113 | */ 114 | 115 | b, 116 | strong { 117 | font-weight: bolder; 118 | } 119 | 120 | /* 121 | 1. Use the user's configured `mono` font family by default. 122 | 2. Correct the odd `em` font sizing in all browsers. 123 | */ 124 | 125 | code, 126 | kbd, 127 | samp, 128 | pre { 129 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 130 | /* 1 */ 131 | font-size: 1em; 132 | /* 2 */ 133 | } 134 | 135 | /* 136 | Add the correct font size in all browsers. 137 | */ 138 | 139 | small { 140 | font-size: 80%; 141 | } 142 | 143 | /* 144 | Prevent `sub` and `sup` elements from affecting the line height in all browsers. 145 | */ 146 | 147 | sub, 148 | sup { 149 | font-size: 75%; 150 | line-height: 0; 151 | position: relative; 152 | vertical-align: baseline; 153 | } 154 | 155 | sub { 156 | bottom: -0.25em; 157 | } 158 | 159 | sup { 160 | top: -0.5em; 161 | } 162 | 163 | /* 164 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) 165 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) 166 | 3. Remove gaps between table borders by default. 167 | */ 168 | 169 | table { 170 | text-indent: 0; 171 | /* 1 */ 172 | border-color: inherit; 173 | /* 2 */ 174 | border-collapse: collapse; 175 | /* 3 */ 176 | } 177 | 178 | /* 179 | 1. Change the font styles in all browsers. 180 | 2. Remove the margin in Firefox and Safari. 181 | 3. Remove default padding in all browsers. 182 | */ 183 | 184 | button, 185 | input, 186 | optgroup, 187 | select, 188 | textarea { 189 | font-family: inherit; 190 | /* 1 */ 191 | font-size: 100%; 192 | /* 1 */ 193 | font-weight: inherit; 194 | /* 1 */ 195 | line-height: inherit; 196 | /* 1 */ 197 | color: inherit; 198 | /* 1 */ 199 | margin: 0; 200 | /* 2 */ 201 | padding: 0; 202 | /* 3 */ 203 | } 204 | 205 | /* 206 | Remove the inheritance of text transform in Edge and Firefox. 207 | */ 208 | 209 | button, 210 | select { 211 | text-transform: none; 212 | } 213 | 214 | /* 215 | 1. Correct the inability to style clickable types in iOS and Safari. 216 | 2. Remove default button styles. 217 | */ 218 | 219 | button, 220 | [type='button'], 221 | [type='reset'], 222 | [type='submit'] { 223 | -webkit-appearance: button; 224 | /* 1 */ 225 | background-color: transparent; 226 | /* 2 */ 227 | background-image: none; 228 | /* 2 */ 229 | } 230 | 231 | /* 232 | Use the modern Firefox focus style for all focusable elements. 233 | */ 234 | 235 | :-moz-focusring { 236 | outline: auto; 237 | } 238 | 239 | /* 240 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) 241 | */ 242 | 243 | :-moz-ui-invalid { 244 | box-shadow: none; 245 | } 246 | 247 | /* 248 | Add the correct vertical alignment in Chrome and Firefox. 249 | */ 250 | 251 | progress { 252 | vertical-align: baseline; 253 | } 254 | 255 | /* 256 | Correct the cursor style of increment and decrement buttons in Safari. 257 | */ 258 | 259 | ::-webkit-inner-spin-button, 260 | ::-webkit-outer-spin-button { 261 | height: auto; 262 | } 263 | 264 | /* 265 | 1. Correct the odd appearance in Chrome and Safari. 266 | 2. Correct the outline style in Safari. 267 | */ 268 | 269 | [type='search'] { 270 | -webkit-appearance: textfield; 271 | /* 1 */ 272 | outline-offset: -2px; 273 | /* 2 */ 274 | } 275 | 276 | /* 277 | Remove the inner padding in Chrome and Safari on macOS. 278 | */ 279 | 280 | ::-webkit-search-decoration { 281 | -webkit-appearance: none; 282 | } 283 | 284 | /* 285 | 1. Correct the inability to style clickable types in iOS and Safari. 286 | 2. Change font properties to `inherit` in Safari. 287 | */ 288 | 289 | ::-webkit-file-upload-button { 290 | -webkit-appearance: button; 291 | /* 1 */ 292 | font: inherit; 293 | /* 2 */ 294 | } 295 | 296 | /* 297 | Add the correct display in Chrome and Safari. 298 | */ 299 | 300 | summary { 301 | display: list-item; 302 | } 303 | 304 | /* 305 | Removes the default spacing and border for appropriate elements. 306 | */ 307 | 308 | blockquote, 309 | dl, 310 | dd, 311 | h1, 312 | h2, 313 | h3, 314 | h4, 315 | h5, 316 | h6, 317 | hr, 318 | figure, 319 | p, 320 | pre { 321 | margin: 0; 322 | } 323 | 324 | fieldset { 325 | margin: 0; 326 | padding: 0; 327 | } 328 | 329 | legend { 330 | padding: 0; 331 | } 332 | 333 | ol, 334 | ul, 335 | menu { 336 | list-style: none; 337 | margin: 0; 338 | padding: 0; 339 | } 340 | 341 | /* 342 | Prevent resizing textareas horizontally by default. 343 | */ 344 | 345 | textarea { 346 | resize: vertical; 347 | } 348 | 349 | /* 350 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) 351 | 2. Set the default placeholder color to the user's configured gray 400 color. 352 | */ 353 | 354 | input::-moz-placeholder, textarea::-moz-placeholder { 355 | opacity: 1; 356 | /* 1 */ 357 | color: #9ca3af; 358 | /* 2 */ 359 | } 360 | 361 | input::placeholder, 362 | textarea::placeholder { 363 | opacity: 1; 364 | /* 1 */ 365 | color: #9ca3af; 366 | /* 2 */ 367 | } 368 | 369 | /* 370 | Set the default cursor for buttons. 371 | */ 372 | 373 | button, 374 | [role="button"] { 375 | cursor: pointer; 376 | } 377 | 378 | /* 379 | Make sure disabled buttons don't get the pointer cursor. 380 | */ 381 | 382 | :disabled { 383 | cursor: default; 384 | } 385 | 386 | /* 387 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) 388 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) 389 | This can trigger a poorly considered lint error in some tools but is included by design. 390 | */ 391 | 392 | img, 393 | svg, 394 | video, 395 | canvas, 396 | audio, 397 | iframe, 398 | embed, 399 | object { 400 | display: block; 401 | /* 1 */ 402 | vertical-align: middle; 403 | /* 2 */ 404 | } 405 | 406 | /* 407 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) 408 | */ 409 | 410 | img, 411 | video { 412 | max-width: 100%; 413 | height: auto; 414 | } 415 | 416 | /* Make elements with the HTML hidden attribute stay hidden by default */ 417 | 418 | [hidden] { 419 | display: none; 420 | } 421 | 422 | *, ::before, ::after { 423 | --tw-border-spacing-x: 0; 424 | --tw-border-spacing-y: 0; 425 | --tw-translate-x: 0; 426 | --tw-translate-y: 0; 427 | --tw-rotate: 0; 428 | --tw-skew-x: 0; 429 | --tw-skew-y: 0; 430 | --tw-scale-x: 1; 431 | --tw-scale-y: 1; 432 | --tw-pan-x: ; 433 | --tw-pan-y: ; 434 | --tw-pinch-zoom: ; 435 | --tw-scroll-snap-strictness: proximity; 436 | --tw-ordinal: ; 437 | --tw-slashed-zero: ; 438 | --tw-numeric-figure: ; 439 | --tw-numeric-spacing: ; 440 | --tw-numeric-fraction: ; 441 | --tw-ring-inset: ; 442 | --tw-ring-offset-width: 0px; 443 | --tw-ring-offset-color: #fff; 444 | --tw-ring-color: rgb(59 130 246 / 0.5); 445 | --tw-ring-offset-shadow: 0 0 #0000; 446 | --tw-ring-shadow: 0 0 #0000; 447 | --tw-shadow: 0 0 #0000; 448 | --tw-shadow-colored: 0 0 #0000; 449 | --tw-blur: ; 450 | --tw-brightness: ; 451 | --tw-contrast: ; 452 | --tw-grayscale: ; 453 | --tw-hue-rotate: ; 454 | --tw-invert: ; 455 | --tw-saturate: ; 456 | --tw-sepia: ; 457 | --tw-drop-shadow: ; 458 | --tw-backdrop-blur: ; 459 | --tw-backdrop-brightness: ; 460 | --tw-backdrop-contrast: ; 461 | --tw-backdrop-grayscale: ; 462 | --tw-backdrop-hue-rotate: ; 463 | --tw-backdrop-invert: ; 464 | --tw-backdrop-opacity: ; 465 | --tw-backdrop-saturate: ; 466 | --tw-backdrop-sepia: ; 467 | } 468 | 469 | ::backdrop { 470 | --tw-border-spacing-x: 0; 471 | --tw-border-spacing-y: 0; 472 | --tw-translate-x: 0; 473 | --tw-translate-y: 0; 474 | --tw-rotate: 0; 475 | --tw-skew-x: 0; 476 | --tw-skew-y: 0; 477 | --tw-scale-x: 1; 478 | --tw-scale-y: 1; 479 | --tw-pan-x: ; 480 | --tw-pan-y: ; 481 | --tw-pinch-zoom: ; 482 | --tw-scroll-snap-strictness: proximity; 483 | --tw-ordinal: ; 484 | --tw-slashed-zero: ; 485 | --tw-numeric-figure: ; 486 | --tw-numeric-spacing: ; 487 | --tw-numeric-fraction: ; 488 | --tw-ring-inset: ; 489 | --tw-ring-offset-width: 0px; 490 | --tw-ring-offset-color: #fff; 491 | --tw-ring-color: rgb(59 130 246 / 0.5); 492 | --tw-ring-offset-shadow: 0 0 #0000; 493 | --tw-ring-shadow: 0 0 #0000; 494 | --tw-shadow: 0 0 #0000; 495 | --tw-shadow-colored: 0 0 #0000; 496 | --tw-blur: ; 497 | --tw-brightness: ; 498 | --tw-contrast: ; 499 | --tw-grayscale: ; 500 | --tw-hue-rotate: ; 501 | --tw-invert: ; 502 | --tw-saturate: ; 503 | --tw-sepia: ; 504 | --tw-drop-shadow: ; 505 | --tw-backdrop-blur: ; 506 | --tw-backdrop-brightness: ; 507 | --tw-backdrop-contrast: ; 508 | --tw-backdrop-grayscale: ; 509 | --tw-backdrop-hue-rotate: ; 510 | --tw-backdrop-invert: ; 511 | --tw-backdrop-opacity: ; 512 | --tw-backdrop-saturate: ; 513 | --tw-backdrop-sepia: ; 514 | } 515 | 516 | .bg-blue-50 { 517 | --tw-bg-opacity: 1; 518 | background-color: rgb(239 246 255 / var(--tw-bg-opacity)); 519 | } 520 | 521 | .bg-rose-500 { 522 | --tw-bg-opacity: 1; 523 | background-color: rgb(244 63 94 / var(--tw-bg-opacity)); 524 | } 525 | 526 | .text-white { 527 | --tw-text-opacity: 1; 528 | color: rgb(255 255 255 / var(--tw-text-opacity)); 529 | } 530 | 531 | .hover\:bg-blue-50:hover { 532 | --tw-bg-opacity: 1; 533 | background-color: rgb(239 246 255 / var(--tw-bg-opacity)); 534 | } 535 | -------------------------------------------------------------------------------- /test-project/css/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /test-project/regen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -x 5 | 6 | pushd .. 7 | cargo build -p tailwindcss-to-rust 8 | popd 9 | ../target/debug/tailwindcss-to-rust \ 10 | --tailwind-config tailwind.config.js \ 11 | --input ./css/tailwind.css \ 12 | --output ./src/generated.rs \ 13 | --rustfmt 14 | tailwindcss --input ./css/tailwind.css --output ./assets/tailwind_compiled.css 15 | 16 | -------------------------------------------------------------------------------- /test-project/src/main.rs: -------------------------------------------------------------------------------- 1 | mod generated; 2 | 3 | use generated::{C, M}; 4 | use tailwindcss_to_rust_macros::{ToOptionVecString, C, M}; 5 | 6 | fn main() { 7 | println!("{}", C::bg::bg_rose_500); 8 | println!("{}", M::hover); 9 | println!( 10 | "{}", 11 | C![M![M::hover, C::bg::bg_blue_50], C::typ::text_white], 12 | ); 13 | } 14 | 15 | // #[actix_web::main] 16 | // async fn main() -> std::io::Result<()> { 17 | // HttpServer::new(|| { 18 | // App::new().service(hello).service( 19 | // fs::Files::new("/", "./public") 20 | // .show_files_listing() 21 | // .use_last_modified(true), 22 | // ) 23 | // }) 24 | // .bind(("0.0.0.0", 7373))? 25 | // .run() 26 | // .await 27 | // } 28 | 29 | // #[get("/")] 30 | // async fn hello() -> impl Responder { 31 | // HttpResponse::Ok().body(html!( 32 | // 33 | // 34 | // "Stuff a ndthigns" 35 | // 36 | // 37 | // 38 | //
"Is this "{hey("italicised?")}
39 | //
40 | //
...
41 | // 42 | // 43 | // )) 44 | // } 45 | -------------------------------------------------------------------------------- /test-project/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: { 4 | files: ["index.html", "**/*.rs"], 5 | extract: { 6 | rs: (content) => { 7 | const rs_to_tw = (rs) => { 8 | if (rs.startsWith("two_")) { 9 | rs = rs.replace("two_", "2"); 10 | } 11 | return rs 12 | .replaceAll("_of_", "/") 13 | .replaceAll("_p_", ".") 14 | .replaceAll("_", "-"); 15 | }; 16 | 17 | let one_class_re = "\\bC::[a-z0-9_]+::([a-z0-9_]+)\\b"; 18 | let class_re = new RegExp(one_class_re, "g"); 19 | let one_mod_re = "\\bM::([a-z0-9_]+)\\b"; 20 | let mod_re = new RegExp(one_mod_re + ", " + one_class_re, "g"); 21 | 22 | let classes = []; 23 | let matches = [...content.matchAll(mod_re)]; 24 | if (matches.length > 0) { 25 | classes.push( 26 | ...matches.map((m) => { 27 | let pieces = m.slice(1, m.length); 28 | return pieces.map((p) => rs_to_tw(p)).join(":"); 29 | }) 30 | ); 31 | } 32 | classes.push( 33 | ...[...content.matchAll(class_re)].map((m) => { 34 | return rs_to_tw(m[1]); 35 | }) 36 | ); 37 | 38 | return classes; 39 | }, 40 | }, 41 | }, 42 | theme: { 43 | extend: {}, 44 | }, 45 | plugins: [], 46 | }; 47 | --------------------------------------------------------------------------------