#tailwind #css-class #generate #completion #compile-time #front-end #names

app tailwindcss-to-rust

Generate Rust code from your compiled tailwind CSS

9 releases

0.3.2 Feb 19, 2023
0.3.1 Feb 19, 2023
0.2.0 Feb 5, 2023
0.1.4 Apr 24, 2022
0.1.3 Feb 17, 2022

#321 in Web programming

Apache-2.0 OR MIT

390KB
8K SLoC

The tailwindcss-to-rust CLI tool generates Rust code that allows you to refer to Tailwind classes from your Rust code. This means that any attempt to use a nonexistent class will lead to a compile-time error, and you can use code completion to list available classes.

This tool has been tested with version 3.2.x of Tailwind.

The generated code allows you to use Tailwind CSS classes in your Rust frontend code with compile-time checking of names and code completion for class names. These classes are grouped together based on the heading in the Tailwind docs. It also generates code for the full list of Tailwind modifiers like lg, hover, etc.

Check out the tailwindcss-to-rust-macros crate for the most ergonomic way to use the code generated by this tool.

So instead of this:

let class = "pt-4 pb-2 text-whit";

You can write this:

let class = C![C::spc::pt_4 C::pb_2 C::typ::text_white];

Note that the typo in the first example, "text-whit" (missing the "e") would become a compile-time error if you wrote C::typ::text_whit.

Here's a quick start recipe:

  1. Install this tool by running:

    cargo install tailwindcss-to-rust
    
  2. Install the tailwindcss CLI tool. You can install it with npm or npx, or you can download a standalone binary from the tailwindcss repo.

  3. Create a tailwind.config.js file with the tool by running:

    tailwindcss init
    
  4. Edit this file however you like to add plugins or customize the generated CSS.

  5. Create a CSS input file for Tailwind. For the purposes of this example we will assume that it's located at css/tailwind.css. The standard file looks like this:

    @tailwind base;
    @tailwind components;
    @tailwind utilities;
    
  6. Generate your Rust code by running:

    tailwindcss-to-rust \
         --tailwind-config tailwind.config.js \
         --input tailwind.css \
         --output src/css/generated.rs \
         --rustfmt
    

    The tailwindcss executable must be in your PATH when you run tailwindcss-to-rust or you must provide the path to the executable in the --tailwindcss argument.

  7. Edit your tailwind.config.js file to look in your Rust files for Tailwind class names:

    /** @type {import('tailwindcss').Config} */
    module.exports = {
      content: {
        files: ["index.html", "**/*.rs"],
        // You do need to copy this big block of code in, unfortunately.
        extract: {
          rs: (content) => {
            const rs_to_tw = (rs) => {
              if (rs.startsWith("two_")) {
                rs = rs.replace("two_", "2");
              }
              return rs
                .replaceAll("_of_", "/")
                .replaceAll("_p_", ".")
                .replaceAll("_", "-");
            };
    
            let one_class_re = "\\bC::[a-z0-9_]+::([a-z0-9_]+)\\b";
            let class_re = new RegExp(one_class_re, "g");
            let one_mod_re = "\\bM::([a-z0-9_]+)\\b";
            let mod_re = new RegExp(one_mod_re + ", " + one_class_re, "g");
    
            let classes = [];
            let matches = [...content.matchAll(mod_re)];
            if (matches.length > 0) {
              classes.push(
                ...matches.map((m) => {
                  let pieces = m.slice(1, m.length);
                  return pieces.map((p) => rs_to_tw(p)).join(":");
                })
              );
            }
            classes.push(
              ...[...content.matchAll(class_re)].map((m) => {
                return rs_to_tw(m[1]);
              })
            );
    
            return classes;
          },
        },
      },
      theme: {
        extend: {},
      },
      plugins: [],
    };
    

    Note that you may need to customize the regexes in the extract function to match your templating system! The regexes in this example will match the syntax you'd use with the tailwindcss-to-rust-macros crate.

    For example, if you're using askama without the macros then you will need to match something like this:

    <div
      class="{{ M::hover }}:{{ C::bg::bg_rose_500 }} {{ C::bg::bg_rose_800 }}"
    >
      ...
    </div>
    

    The regexes for that would look something like this:

    let one_class_re = "{{\\s*C::[a-z0-9_]+::([a-z0-9_]+)\\s*}}";
    let class_re = new RegExp(one_class_re, "g");
    let one_mod_re = "{{\\s*M::([a-z0-9_]+)\\s*}}";
    let mod_re = new RegExp(one_mod_re + ":" + one_class_re, "g");
    
  8. Hack, hack, hack ...

  9. Regenerate your compiled Tailwind CSS file by running:

    tailwindcss --input css/tailwind.css --output css/tailwind_compiled.css`
    
  10. Make sure to import the compiled CSS in your HTML:

    <link data-trunk rel="css" href="/css/tailwind_compiled.css" />
    

In this example, I'm using Trunk, which is a great alternative to webpack for projects that want to use Rust -> WASM without any node.js tooling. My Trunk.toml looks like this:

[build]
target = "index.html"
dist = "dist"

[[hooks]]
stage = "build"
# I'm not sure why we can't just invoke tailwindcss directly, but that doesn't
# seem to work for some reason.
command = "sh"
command_arguments = ["-c", "tailwindcss -i css/tailwind.css -o css/tailwind_compiled.css"]

When I run trunk I have to make sure to ignore that generated file:

trunk --ignore ./css/tailwind_compiled.css ...

The generated names consist of all the class names present in the CSS file, except names that start with a dash (-), names that contain pseudo-elements, like .placeholder-opacity-100::-moz-placeholder, and names that contain modifiers like lg or hover. Names are transformed into Rust identifiers using the following algorithm:

  • All backslash escapes are removed entirely, for example in .inset-0\.5.
  • All dashes (-) become underscores (_).
  • All periods (.) become _p_, so .inset-2\.5 becomes inset_2_p_5.
  • All forward slashes (/) become _of_, so .inset-2\/4 becomes inset_2_of_4.
  • If a name starts with a 2, as in 2xl, it becomes two_, so the 2xl modifier becomes two_xl.
  • The name static becomes static_.

The generated code provides two modules containing all of the relevant strings.

The C module contains a number of submodules, one for each group of classes as documented in the TailwindCSS docs. The groups are as follows:

pub(crate) mod C {
    // Accessibility
    pub(crate) mod acc { ... }

    // Animation
    pub(crate) mod anim { ... }

    // Backgrounds
    pub(crate) mod bg { ... }

    // Borders
    pub(crate) mod bor { ... }

    // Effects
    pub(crate) mod eff { ... }

    // Filter
    pub(crate) mod fil { ... }

    // Flexbox & Grid
    pub(crate) mod fg { ... }

    // Interactivity
    pub(crate) mod intr { ... }

    // Layout
    pub(crate) mod lay { ... }

    // Sizing
    pub(crate) mod siz { ... }

    // Spacing
    pub(crate) mod spc { ... }

    // SVG
    pub(crate) mod svg { ... }

    // Tables
    pub(crate) mod tbl { ... }

    // Transforms
    pub(crate) mod trn { ... }

    // Typography
    pub(crate) mod typ { ... }
}

In your code, you can refer to classes with C::typ::text_lg or C::lay::flex. If you have any custom classes, these will end in an "unknown" group available from C::unk. Adding a way to put these custom classes in other groups is a todo item.

The modifiers have their own module, M, which contains one field per modifier, so it's used as M::lg or M::hover. A few modifiers which are parameterizable are not included, like aria-*, data-*, etc.

The best way to understand the generated modules is to open the generated code file in your editor and look at it.

Then you can import these consts in your code and use them to refer to Tailwind CSS class names with compile time checking:

element.set_class(C::lay::aspect_auto);

Dependencies

~7–18MB
~245K SLoC