#![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 = find_invocation(&source[line_pos..])
.and_then(find_block)
.ok_or("couldn't find css! macro call")?;
Ok(pinkie_parser::parse(block.parse()?).css)
}
fn find_invocation(mut source: &str) -> Option<&str> {
loop {
let idx = source.find("css!")?;
let boundary = source[..idx]
.chars()
.next_back()
.is_none_or(|ch| !ch.is_alphanumeric() && ch != '_');
source = &source[idx + 4..];
if boundary {
let rest = source.trim_start();
if rest.starts_with(['{', '(', '[']) {
return Some(&rest[1..]);
}
}
}
}
fn find_block(input: &str) -> Option<&str> {
let bytes = input.as_bytes();
let mut depth = 1;
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'{' | b'[' | b'(' => depth += 1,
b'}' | b']' | b')' => {
depth -= 1;
if depth == 0 {
return Some(&input[..i]);
}
}
b'/' if bytes.get(i + 1) == Some(&b'/') => {
i += input[i..].find('\n').unwrap_or(input.len() - i);
continue;
}
b'/' if bytes.get(i + 1) == Some(&b'*') => {
let mut comments = 1;
i += 2;
while i < bytes.len() && comments > 0 {
match (bytes[i], bytes.get(i + 1)) {
(b'/', Some(b'*')) => {
comments += 1;
i += 2;
}
(b'*', Some(b'/')) => {
comments -= 1;
i += 2;
}
_ => i += 1,
}
}
continue;
}
b'"' => {
i += 1;
while i < bytes.len() && bytes[i] != b'"' {
i += if bytes[i] == b'\\' { 2 } else { 1 };
}
}
b'r' if matches!(bytes.get(i + 1), Some(b'"' | b'#')) => {
let hashes = bytes[i + 1..].iter().take_while(|&&b| b == b'#').count();
if bytes.get(i + 1 + hashes) == Some(&b'"') {
let close = format!("\"{}", "#".repeat(hashes));
i += 2 + hashes;
i += input[i..].find(&close)? + close.len();
continue;
}
}
b'\'' => {
let rest = &input[i + 1..];
let mut chars = rest.chars();
match chars.next() {
Some('\\') => {
i += 3; while i < bytes.len() && bytes[i] != b'\'' {
i += 1;
}
}
Some(ch) if chars.next() == Some('\'') => {
i += 1 + ch.len_utf8(); }
_ => {} }
}
_ => {}
}
i += 1;
}
None
}
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 {:?}), falling back to static: {e}",
style.location
);
out.push_str(style.css);
}
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn invocation_boundary() {
assert_eq!(find_invocation("css! { a }"), Some(" a }"));
assert_eq!(find_invocation("css!{a}"), Some("a}"));
assert_eq!(find_invocation("css!(a)"), Some("a)"));
assert_eq!(find_invocation("pinkie::css! { a }"), Some(" a }"));
assert_eq!(find_invocation("my_css! { a } css! { b }"), Some(" b }"));
assert_eq!(find_invocation("scss! { a }"), None);
assert_eq!(find_invocation("no invocation"), None);
assert_eq!(find_invocation("\"css!\" css! { a }"), Some(" a }"));
}
#[test]
fn block_delimiters() {
assert_eq!(find_block("color: red; } after"), Some("color: red; "));
assert_eq!(find_block("a { b { } } }"), Some("a { b { } } "));
assert_eq!(find_block("mixed ([{}]) }"), Some("mixed ([{}]) "));
assert_eq!(find_block("unterminated {"), None);
}
#[test]
fn block_skips_comments() {
assert_eq!(find_block("// }\n }"), Some("// }\n "));
assert_eq!(find_block("// } no newline"), None);
assert_eq!(find_block("/* } */ }"), Some("/* } */ "));
assert_eq!(find_block("/* /* } */ } */ }"), Some("/* /* } */ } */ "));
assert_eq!(find_block("a / b }"), Some("a / b "));
}
#[test]
fn block_skips_strings() {
assert_eq!(find_block(r#" "}" }"#), Some(r#" "}" "#));
assert_eq!(find_block(r#" "\"}" }"#), Some(r#" "\"}" "#));
assert_eq!(find_block(r###" r"}" }"###), Some(r###" r"}" "###));
assert_eq!(find_block(r###" r#"}"# }"###), Some(r###" r#"}"# "###));
assert_eq!(find_block(r###" r##"}"# "## }"###), Some(r###" r##"}"# "## "###));
assert_eq!(find_block("var(--r) }"), Some("var(--r) "));
}
#[test]
fn block_skips_char_literals() {
assert_eq!(find_block("'}' }"), Some("'}' "));
assert_eq!(find_block(r"'\'' }"), Some(r"'\'' "));
assert_eq!(find_block(r"'\u{7d}' }"), Some(r"'\u{7d}' "));
assert_eq!(find_block("&'a () }"), Some("&'a () "));
}
}
}