encre_css/plugins/
mod.rs

1//! A [`Plugin`] is a handler used to convert utility classes into CSS declarations.
2//!
3//! A lot of plugins are built in (like the ones from Tailwind CSS) and some others live in
4//! their own crates and need to be imported manually. They usually define a `register` function taking
5//! a mutable reference to a [`Config`] structure.
6//!
7//! # Example (with `encre-css-typography`)
8//!
9//! ```ignore
10//! use encre_css::Config;
11//!
12//! # fn main() -> encre_css::Result<()> {
13//! let mut config = Config::from_file("encre-css.toml")?;
14//! // Or let mut config = Config::default();
15//!
16//! encre_css_typography::register(&mut config);
17//!
18//! let _css = encre_css::generate(
19//!     [r#"<div class="prose prose-headings:text-blue-500 prose-slate lg:prose-lg dark:prose-invert"></div>"#],
20//!     &config,
21//! );
22//! // Do something with the CSS
23//! # Ok(())
24//! # }
25//! ```
26//!
27//! # Official plugins
28//!
29//! - [`encre-css-typography`](https://gitlab.com/encre-org/encre-css/tree/main/crates/encre-css-typography): used to define beautiful typographic defaults for HTML you don't control.
30//! - [`encre-css-icons`](https://gitlab.com/encre-org/encre-css/tree/main/crates/encre-css-icons): used to quickly add pure CSS icons to your website.
31//!
32//! If you want to write your own plugins, see [`Plugin`].
33//!
34//! [`Config`]: crate::Config
35use crate::generator::{ContextCanHandle, ContextHandle};
36
37use std::fmt;
38
39pub mod accessibility;
40pub mod background;
41pub mod border;
42pub mod css_property;
43pub mod effect;
44pub mod filter;
45pub mod flexbox;
46pub mod grid;
47pub mod interactivity;
48pub mod layout;
49pub mod sizing;
50pub mod spacing;
51pub mod svg;
52pub mod table;
53pub mod transform;
54pub mod transition;
55pub mod typography;
56
57/// A plugin is a structure capable of generating CSS styles from a modifier (contained in a
58/// context structure).
59///
60/// Each plugin consists of two methods:
61/// - [`Plugin::can_handle`] to check if it will be able to generate CSS for a specific modifier;
62/// - [`Plugin::handle`] to generate the CSS needed.
63///
64/// The [`Plugin::can_handle`] method takes a [`ContextCanHandle`] structure containing the
65/// modifier and the current configuration.
66///
67/// The [`Plugin::handle`] method takes a [`ContextHandle`] structure containing the modifier,
68/// the current configuration and a buffer containing the whole CSS
69/// currently generated. You can use the [`Buffer`] structure (especially the [`Buffer::line`]
70/// and [`Buffer::lines`] functions) to push CSS declarations to it, they will be automatically
71/// indented.
72///
73/// It is common to use the [`unreachable!`] macro if the [`Plugin::handle`] method cannot be
74/// called because you are sure that [`Plugin::can_handle`] returned `false`.
75///
76/// # Example (defines the `stroke-width` plugin)
77///
78/// ```
79/// use encre_css::prelude::build_plugin::*;
80///
81/// #[derive(Debug)]
82/// struct StrokeWidth;
83///
84/// impl Plugin for StrokeWidth {
85///     fn can_handle(&self, context: ContextCanHandle) -> bool {
86///         match context.modifier {
87///             Modifier::Builtin { value, .. } => value.parse::<usize>().is_ok(),
88///             Modifier::Arbitrary { hint, value, .. } => {
89///                 *hint == "length"
90///                     || *hint == "number"
91///                     || *hint == "percentage"
92///                     || (hint.is_empty()
93///                         && (is_matching_length(value) || is_matching_percentage(value)))
94///             }
95///         }
96///     }
97///
98///     fn handle(&self, context: &mut ContextHandle) {
99///         match context.modifier {
100///             Modifier::Builtin { value, .. } => {
101///                 context.buffer.line(format_args!("stroke-width: {value}px;"));
102///             }
103///             Modifier::Arbitrary { value, .. } => {
104///                 context.buffer.line(format_args!("stroke-width: {value};"));
105///             }
106///         }
107///     }
108/// }
109/// ```
110///
111/// # Release a plugin as a crate
112///
113/// If you want to release your custom plugins as a crate, you can export a `register` function
114/// taking a mutable reference to a [`Config`] structure and use the [`Config::register_plugin`]
115/// function to register them. The first argument is the namespace prefixing all the
116/// utility classes handled by the plugin.
117///
118/// ```ignore
119/// pub fn register(config: &mut Config) {
120///     config.register_plugin("stroke", &StrokeWidth);
121/// }
122/// ```
123///
124/// # More powerful usage
125///
126/// If you need to have full control over the CSS **rule**, you can create a [`needs_wrapping`]
127/// method returning false and use [`generator::generate_at_rules`], [`generator::generate_class`]
128/// and [`generator::generate_wrapper`] to generate some CSS boilerplate.
129///
130/// ### Example (roughly defines the `animation` plugin)
131///
132/// ```
133/// use encre_css::prelude::build_plugin::*;
134///
135/// #[derive(Debug)]
136/// struct PluginDefinition;
137///
138/// impl Plugin for PluginDefinition {
139///     fn needs_wrapping(&self) -> bool {
140///         false
141///     }
142///
143///     fn can_handle(&self, context: ContextCanHandle) -> bool {
144///         match context.modifier {
145///             Modifier::Builtin { value, .. } => {
146///                 ["spin", "ping", "pulse", "bounce", "none"].contains(value)
147///             }
148///             Modifier::Arbitrary { value, .. } => is_matching_all(value),
149///         }
150///     }
151///
152///     fn handle(&self, context: &mut ContextHandle) {
153///         match context.modifier {
154///             Modifier::Builtin { value, .. } => {
155///                 let animation = match *value {
156///                     "none" => "none",
157///                     "spin" => {
158///                         context.buffer.lines([
159///                             "@keyframes spin",
160///                             "...",
161///                         ]);
162///                         "spin 1s linear infinite"
163///                     }
164///                     "ping" => {
165///                         context.buffer.lines([
166///                             "@keyframes ping",
167///                             "...",
168///                         ]);
169///                         "ping 1s cubic-bezier(0, 0, 0.2, 1) infinite"
170///                     }
171///                     "pulse" => {
172///                         context.buffer.lines([
173///                             "@keyframes pulse",
174///                             "...",
175///                         ]);
176///                         "pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite"
177///                     }
178///                     "bounce" => {
179///                         context.buffer.lines([
180///                             "@keyframes bounce",
181///                             "...",
182///                         ]);
183///                         "bounce 1s infinite"
184///                     }
185///                     _ => unreachable!(),
186///                 };
187///
188///                 generate_wrapper(context, |context| {
189///                     context.buffer.line(format_args!("animation: {animation};"));
190///                 })
191///             }
192///             Modifier::Arbitrary { value, .. } => generate_wrapper(context, |context| {
193///                 context.buffer.line(format_args!("animation: {value};"));
194///             }),
195///         }
196///     }
197/// }
198/// ```
199///
200/// Have a look at <https://gitlab.com/encre-org/encre-css/tree/main/crates/encre-css/src/plugins>
201/// for more examples.
202///
203/// [`Buffer`]: crate::utils::buffer::Buffer
204/// [`Buffer::line`]: crate::utils::buffer::Buffer::line
205/// [`Buffer::lines`]: crate::utils::buffer::Buffer::lines
206/// [`Config::register_plugin`]: crate::Config::register_plugin
207/// [`Config`]: crate::Config
208/// [`needs_wrapping`]: Plugin::needs_wrapping
209/// [`generator::generate_at_rules`]: crate::generator::generate_at_rules
210/// [`generator::generate_class`]: crate::generator::generate_class
211/// [`generator::generate_wrapper`]: crate::generator::generate_wrapper
212pub trait Plugin: fmt::Debug {
213    /// Returns whether the plugin can handle a specific modifier.
214    fn can_handle(&self, _context: ContextCanHandle) -> bool;
215
216    /// Returns whether the plugin should be wrapped inside a CSS rule or if it will manually
217    /// generate it
218    fn needs_wrapping(&self) -> bool {
219        true
220    }
221
222    /// Get the CSS code from a modifier.
223    ///
224    ///
225    /// The [`Plugin::can_handle`] method **must be** called before to know if it can handle
226    /// the modifier, otherwise this function **will panic**.
227    ///
228    /// Various notes:
229    /// - The CSS returned should end with a newline;
230    /// - Arbitrary values are already normalized (e.g. underscores are replaced by spaces);
231    /// - This function is guaranteed to be called only once per selector.
232    fn handle(&self, _context: &mut ContextHandle);
233}