use std::collections::BTreeSet;
#[derive(Debug, Clone, Default)]
pub struct BemStructure {
pub block: Option<String>,
pub elements: BTreeSet<String>,
pub modifiers: BTreeSet<String>,
pub raw_tokens: BTreeSet<String>,
}
pub fn extract_style_tokens(source: &str) -> Vec<StyleToken> {
let mut tokens = Vec::new();
let mut seen = BTreeSet::new();
let bytes = source.as_bytes();
let len = bytes.len();
let mut i = 0;
while i < len {
if i + 7 <= len && &bytes[i..i + 7] == b"styles." {
if i > 0 && is_ident_char(bytes[i - 1]) {
i += 1;
continue;
}
let start = i + 7;
if start >= len || !is_ident_start(bytes[start]) {
i += 1;
continue;
}
let first_end = read_ident(bytes, start);
let first = &source[start..first_end];
if first == "modifiers" {
if first_end < len && bytes[first_end] == b'.' {
let mod_start = first_end + 1;
if mod_start < len && is_ident_start(bytes[mod_start]) {
let mod_end = read_ident(bytes, mod_start);
let modifier = source[mod_start..mod_end].to_string();
let key = format!("modifiers.{modifier}");
if seen.insert(key) {
tokens.push(StyleToken::Modifier(modifier));
}
}
}
} else {
let token = first.to_string();
if seen.insert(token.clone()) {
tokens.push(StyleToken::ClassToken(token));
}
}
i = first_end;
} else {
i += 1;
}
}
tokens
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StyleToken {
ClassToken(String),
Modifier(String),
}
pub fn extract_bem_block_from_import(source: &str) -> Option<String> {
for line in source.lines() {
let trimmed = line.trim();
if !trimmed.starts_with("import ") {
continue;
}
if !trimmed.contains("@patternfly/react-styles/css/") {
continue;
}
let after_import = trimmed.strip_prefix("import ")?;
let binding = after_import.split_whitespace().next()?;
if binding != "styles" {
continue;
}
let quote_start = trimmed.rfind('\'')?;
let path = &trimmed[..quote_start];
let block = path.rsplit('/').next()?;
return Some(block.to_string());
}
None
}
pub fn parse_bem_structure(tokens: &[StyleToken], block_override: Option<&str>) -> BemStructure {
let mut structure = BemStructure::default();
let mut class_tokens: Vec<&str> = Vec::new();
for token in tokens {
match token {
StyleToken::ClassToken(name) => {
structure.raw_tokens.insert(name.clone());
class_tokens.push(name);
}
StyleToken::Modifier(name) => {
structure.modifiers.insert(name.clone());
}
}
}
if class_tokens.is_empty() {
return structure;
}
let block = if let Some(override_block) = block_override {
override_block.to_string()
} else {
class_tokens.sort_by_key(|t| t.len());
class_tokens[0].to_string()
};
structure.block = Some(block.clone());
for token in &class_tokens {
if *token == block {
continue;
}
if token.starts_with(&block) {
let suffix = &token[block.len()..];
if suffix.starts_with(|c: char| c.is_uppercase()) {
let element = lowercase_first(suffix);
structure.elements.insert(element);
}
}
}
structure
}
pub fn classify_bem_relationship(
child_block: Option<&str>,
child_tokens: &BTreeSet<String>,
parent_block: &str,
) -> BemRelationship {
for token in child_tokens {
if let Some(suffix) = token.strip_prefix(parent_block) {
if suffix.starts_with(|c: char| c.is_uppercase()) {
return BemRelationship::Element {
element_name: lowercase_first(suffix),
};
}
}
}
if let Some(block) = child_block {
if block != parent_block {
return BemRelationship::Independent {
block_name: block.to_string(),
};
}
}
BemRelationship::Unknown
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BemRelationship {
Element { element_name: String },
Independent { block_name: String },
Unknown,
}
fn is_ident_char(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'_' || b == b'$'
}
fn is_ident_start(b: u8) -> bool {
b.is_ascii_alphabetic() || b == b'_' || b == b'$'
}
fn read_ident(bytes: &[u8], start: usize) -> usize {
let mut end = start;
while end < bytes.len() && is_ident_char(bytes[end]) {
end += 1;
}
end
}
fn lowercase_first(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_lowercase().to_string() + chars.as_str(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_style_tokens() {
let source = r#"
<div className={css(styles.menu, isPlain && styles.modifiers.plain)}>
<ul className={css(styles.menuList)}>
<li className={css(styles.menuListItem, styles.modifiers.disabled)}>
<span className={css(styles.menuItemMain)} />
</li>
</ul>
</div>
"#;
let tokens = extract_style_tokens(source);
assert!(tokens.contains(&StyleToken::ClassToken("menu".into())));
assert!(tokens.contains(&StyleToken::ClassToken("menuList".into())));
assert!(tokens.contains(&StyleToken::ClassToken("menuListItem".into())));
assert!(tokens.contains(&StyleToken::ClassToken("menuItemMain".into())));
assert!(tokens.contains(&StyleToken::Modifier("plain".into())));
assert!(tokens.contains(&StyleToken::Modifier("disabled".into())));
}
#[test]
fn test_parse_bem_structure() {
let tokens = vec![
StyleToken::ClassToken("menu".into()),
StyleToken::ClassToken("menuList".into()),
StyleToken::ClassToken("menuListItem".into()),
StyleToken::ClassToken("menuItemMain".into()),
StyleToken::Modifier("expanded".into()),
StyleToken::Modifier("disabled".into()),
];
let bem = parse_bem_structure(&tokens, None);
assert_eq!(bem.block, Some("menu".into()));
assert!(bem.elements.contains("list"));
assert!(bem.elements.contains("listItem"));
assert!(bem.elements.contains("itemMain"));
assert!(bem.modifiers.contains("expanded"));
assert!(bem.modifiers.contains("disabled"));
}
#[test]
fn test_parse_bem_modal_box() {
let tokens = vec![
StyleToken::ClassToken("modalBox".into()),
StyleToken::ClassToken("modalBoxBody".into()),
StyleToken::ClassToken("modalBoxHeader".into()),
StyleToken::ClassToken("modalBoxFooter".into()),
StyleToken::ClassToken("modalBoxDescription".into()),
StyleToken::ClassToken("modalBoxClose".into()),
];
let bem = parse_bem_structure(&tokens, None);
assert_eq!(bem.block, Some("modalBox".into()));
assert!(bem.elements.contains("body"));
assert!(bem.elements.contains("header"));
assert!(bem.elements.contains("footer"));
assert!(bem.elements.contains("description"));
assert!(bem.elements.contains("close"));
}
#[test]
fn test_classify_bem_element() {
let child_tokens: BTreeSet<String> = vec!["menuList".to_string()].into_iter().collect();
let rel = classify_bem_relationship(None, &child_tokens, "menu");
assert_eq!(
rel,
BemRelationship::Element {
element_name: "list".into()
}
);
}
#[test]
fn test_extract_style_tokens_with_utf8() {
let source = r#"
/**
* © Copyright 2024 Company
* Licensed under Apache License
*/
import styles from './component.css';
const Component = () => (
<div className={styles.menu}>
<span className={styles.menuList} />
</div>
);
"#;
let tokens = extract_style_tokens(source);
assert!(tokens.contains(&StyleToken::ClassToken("menu".into())));
assert!(tokens.contains(&StyleToken::ClassToken("menuList".into())));
}
#[test]
fn test_classify_bem_independent() {
let child_tokens: BTreeSet<String> = vec!["menuToggle".to_string()].into_iter().collect();
let rel = classify_bem_relationship(Some("menuToggle"), &child_tokens, "dropdown");
assert_eq!(
rel,
BemRelationship::Independent {
block_name: "menuToggle".into()
}
);
}
}