#![doc = include_str!("../readme.md")]
#![cfg_attr(not(all(debug_assertions, feature = "dynamic")), no_std)]
#![deny(missing_docs)]
extern crate alloc;
use alloc::{collections::BTreeMap, string::String};
use core::fmt::{self, Debug, Display};
pub use pinkie_macros::css;
#[doc(hidden)]
pub use inventory::submit as __submit;
#[cfg(feature = "location")]
#[derive(Clone)]
pub struct Location {
pub file: &'static str,
pub line: usize,
}
#[cfg(feature = "location")]
impl Debug for Location {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.file, self.line)
}
}
#[derive(Debug, Clone)]
pub struct Style {
pub class: &'static str,
pub css: &'static str,
#[cfg(feature = "location")]
pub location: Location,
}
impl Display for Style {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.class)
}
}
inventory::collect!(Style);
pub fn styles() -> impl Iterator<Item = &'static Style> {
inventory::iter::<Style>()
}
#[inline]
fn collect_impl(mut write: impl FnMut(&'static Style, &mut String)) -> String {
let mut joined = String::new();
let mut visited = BTreeMap::new();
for style in styles() {
if let Some(visited) = visited.insert(style.class, style) {
if visited.css == style.css {
continue;
}
#[cfg(feature = "location")]
panic!(
"duplicate class (hash collision): {}, at {:?} and {:?}",
style.class, style.location, visited.location
);
#[cfg(not(feature = "location"))]
panic!("duplicate class (hash collision): {}", style.class);
}
joined.push('.');
joined.push_str(style.class);
joined.push('{');
write(style, &mut joined);
joined.push_str("}\n");
}
joined
}
#[cfg(not(all(debug_assertions, feature = "dynamic")))]
pub fn collect() -> String {
collect_impl(|style, res| res.push_str(&style.css))
}
#[cfg(all(debug_assertions, feature = "dynamic"))]
pub use dynamic::collect;
#[cfg(all(debug_assertions, feature = "dynamic"))]
mod dynamic {
use super::*;
use std::{
collections::{hash_map::Entry, HashMap},
error::Error,
io::ErrorKind,
};
fn collect_dynamic(
style: &Style,
files: &mut HashMap<&str, String>,
) -> Result<String, Box<dyn Error>> {
let source = match files.entry(style.location.file) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
let source = match std::fs::read_to_string(entry.key()) {
Err(e) if e.kind() == ErrorKind::NotFound => {
return Err(format!("file {} not found", entry.key()).into())
}
r => r?,
};
entry.insert(source)
}
};
let line_pos: usize = source
.split_inclusive('\n')
.take(style.location.line.saturating_sub(1))
.map(|line| line.len())
.sum();
let block = &source[line_pos..]
.split_once("css!")
.and_then(|(_, rest)| {
rest.trim_start()
.strip_prefix(|ch| matches!(ch, '{' | '(' | '['))
})
.and_then(|input| {
let mut depth = 1;
for (i, ch) in input.char_indices() {
match ch {
'{' | '[' | '(' => depth += 1,
'}' | ']' | ')' => depth -= 1,
_ => {}
}
if depth == 0 {
return Some(&input[..i]);
}
}
None
})
.ok_or("couldn't find css! macro call")?;
Ok(pinkie_parser::parse(block.parse()?).css)
}
pub fn collect() -> String {
let mut files = Default::default();
collect_impl(|style, out| match collect_dynamic(style, &mut files) {
Ok(s) => out.push_str(&s),
Err(e) => log::warn!(
"dynamic css error (css! macro at {:?}): {e}",
style.location
),
})
}
}