pinkie/
lib.rs

1#![doc = include_str!("../readme.md")]
2#![cfg_attr(not(all(debug_assertions, feature = "dynamic")), no_std)]
3#![deny(missing_docs)]
4
5extern crate alloc;
6
7use alloc::{collections::BTreeMap, string::String};
8use core::fmt::{self, Debug, Display};
9
10/// The macro used to define scoped CSS styles.
11///
12/// It returns a [`Style`], the `Display` implementation of which writes
13/// out the class name.
14pub use pinkie_macros::css;
15
16/// Re-export of `inventory::submit` that's used by the macro.
17/// Not intended to be used directly.
18///
19/// Nothing _stops_ you, of course, but those two underscores sure are ugly,
20/// aren't they?
21#[doc(hidden)]
22pub use inventory::submit as __submit;
23
24/// A simple location in the source code.
25#[cfg(feature = "location")]
26#[derive(Clone)]
27pub struct Location {
28    /// Name of the file as returned by the `file!()` macro.
29    pub file: &'static str,
30    /// Line number as returned by the `line!()` macro.
31    pub line: usize,
32}
33
34#[cfg(feature = "location")]
35impl Debug for Location {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        write!(f, "{}:{}", self.file, self.line)
38    }
39}
40
41/// A value returned by the `css!` macro.
42///
43/// Usually you'd just use the `Display` implementation to write out the class
44/// name somewhere.
45#[derive(Debug, Clone)]
46pub struct Style {
47    /// The scoping class name - `env!("PINKIE_CSS_CLASS_PREFIX")` (`pinkie-`
48    /// by default) followed by a hash of the generated CSS string.
49    pub class: &'static str,
50    /// The CSS string generated from Rust tokens passed to the `css!` macro.
51    pub css: &'static str,
52    /// The location in the source code where the `css!` macro was called, the
53    /// line number corresponding to the line containing the "css!" string.
54    #[cfg(feature = "location")]
55    pub location: Location,
56}
57
58impl Display for Style {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        f.write_str(&self.class)
61    }
62}
63
64inventory::collect!(Style);
65
66/// Iterate over all `css!` macro invocations.
67pub fn styles() -> impl Iterator<Item = &'static Style> {
68    inventory::iter::<Style>()
69}
70
71#[inline]
72fn collect_impl(mut write: impl FnMut(&'static Style, &mut String)) -> String {
73    let mut joined = String::new();
74    let mut visited = BTreeMap::new();
75    for style in styles() {
76        if let Some(visited) = visited.insert(style.class, style) {
77            if visited.css == style.css {
78                continue;
79            }
80            #[cfg(feature = "location")]
81            panic!(
82                "duplicate class (hash collision): {}, at {:?} and {:?}",
83                style.class, style.location, visited.location
84            );
85            #[cfg(not(feature = "location"))]
86            panic!("duplicate class (hash collision): {}", style.class);
87        }
88        joined.push('.');
89        joined.push_str(style.class);
90        joined.push('{');
91        write(style, &mut joined);
92        joined.push_str("}\n");
93    }
94    joined
95}
96
97/// Collect all `css!` styles into a single string under unique classes.
98///
99/// If the feature `dynamic` and debug assertions are enabled, this will
100/// ignore the static css string generated by the macro, and instead try to
101/// read the Rust source at the location of the `css!` macro calls.
102///
103/// This means that every time this function is called, it will return the most
104/// up-to-date CSS according to the source code, without requiring a
105/// Rust recompilation.
106///
107/// Upgrading it into a hot-reload system is trivial, and left as an exercise
108/// to the reader.
109///
110/// # Panics
111/// Will panic on hash collisions - this can be fixed by slightly adjusting
112/// one of the clashing styles.
113/// Such collisions should be pretty rare.
114#[cfg(not(all(debug_assertions, feature = "dynamic")))]
115pub fn collect() -> String {
116    collect_impl(|style, res| res.push_str(&style.css))
117}
118
119#[cfg(all(debug_assertions, feature = "dynamic"))]
120pub use dynamic::collect;
121
122#[cfg(all(debug_assertions, feature = "dynamic"))]
123mod dynamic {
124    use super::*;
125    use std::{
126        collections::{hash_map::Entry, HashMap},
127        error::Error,
128        io::ErrorKind,
129    };
130
131    fn collect_dynamic(
132        style: &Style,
133        files: &mut HashMap<&str, String>,
134    ) -> Result<String, Box<dyn Error>> {
135        let source = match files.entry(style.location.file) {
136            Entry::Occupied(entry) => entry.into_mut(),
137            Entry::Vacant(entry) => {
138                let source = match std::fs::read_to_string(entry.key()) {
139                    Err(e) if e.kind() == ErrorKind::NotFound => {
140                        return Err(format!("file {} not found", entry.key()).into())
141                    }
142                    r => r?,
143                };
144                entry.insert(source)
145            }
146        };
147
148        let line_pos: usize = source
149            .split_inclusive('\n')
150            .take(style.location.line.saturating_sub(1))
151            .map(|line| line.len())
152            .sum();
153
154        let block = &source[line_pos..]
155            .split_once("css!")
156            .and_then(|(_, rest)| {
157                rest.trim_start()
158                    .strip_prefix(|ch| matches!(ch, '{' | '(' | '['))
159            })
160            .and_then(|input| {
161                let mut depth = 1;
162                for (i, ch) in input.char_indices() {
163                    match ch {
164                        '{' | '[' | '(' => depth += 1,
165                        '}' | ']' | ')' => depth -= 1,
166                        _ => {}
167                    }
168                    if depth == 0 {
169                        return Some(&input[..i]);
170                    }
171                }
172                None
173            })
174            .ok_or("couldn't find css! macro call")?;
175
176        Ok(pinkie_parser::parse(block.parse()?).css)
177    }
178
179    /// Collect all `css!` styles into a single string under unique classes.
180    ///
181    /// If the feature `dynamic` and debug assertions are enabled, this will
182    /// ignore the static css string generated by the macro, and instead try to
183    /// read the Rust source at the location of the `css!` macro calls.
184    ///
185    /// This means that every time this function is called, it will return the most
186    /// up-to-date CSS according to the source code, without requiring a
187    /// Rust recompilation.
188    ///
189    /// Upgrading it into a hot-reload system is trivial, and left as an exercise
190    /// to the reader.
191    ///
192    /// # Panics
193    /// Will panic on hash collisions - this can be fixed by slightly adjusting
194    /// one of the clashing styles.
195    ///
196    /// Such collisions should be pretty rare.
197    pub fn collect() -> String {
198        let mut files = Default::default();
199        collect_impl(|style, out| match collect_dynamic(style, &mut files) {
200            Ok(s) => out.push_str(&s),
201            Err(e) => log::warn!(
202                "dynamic css error (css! macro at {:?}): {e}",
203                style.location
204            ),
205        })
206    }
207}