repo_description: Empowering everyone to build reliable and efficient software.
repo_topics: compiler language rust
code: use std::fs;
  use std::iter::Peekable;
  use std::path::Path;
  use std::str::Chars;
  
  use rustc_data_structures::fx::{FxIndexMap, IndexEntry};
  use rustc_errors::DiagCtxtHandle;
  
  #[cfg(test)]
  mod tests;
  
  #[derive(Debug)]
  pub(crate) struct CssPath {
      pub(crate) rules: FxIndexMap<String, String>,
      pub(crate) children: FxIndexMap<String, CssPath>,
  }
  
  /// When encountering a `"` or a `'`, returns the whole string, including the quote characters.
  fn get_string(iter: &mut Peekable<Chars<'_>>, string_start: char, buffer: &mut String) {
      buffer.push(string_start);
      while let Some(c) = iter.next() {
          buffer.push(c);
          if c == '\\' {
              iter.next();
          } else if c == string_start {
              break;
          }
      }
  }
  
  fn get_inside_paren(
      iter: &mut Peekable<Chars<'_>>,
      paren_start: char,
      paren_end: char,
      buffer: &mut String,
  ) {
      buffer.push(paren_start);
      while let Some(c) = iter.next() {
          handle_common_chars(c, buffer, iter);
          if c == paren_end {
              break;
          }
      }
  }
  
  /// Skips a `/*` comment.
  fn skip_comment(iter: &mut Peekable<Chars<'_>>) {
      while let Some(c) = iter.next() {
          if c == '*' && iter.next() == Some('/') {
              break;
          }
      }
  }
  
  /// Skips a line comment (`//`).
  fn skip_line_comment(iter: &mut Peekable<Chars<'_>>) {
      for c in iter.by_ref() {
          if c == '\n' {
              break;
          }
      }
  }
  
  fn handle_common_chars(c: char, buffer: &mut String, iter: &mut Peekable<Chars<'_>>) {
      match c {
          '"' | '\'' => get_string(iter, c, buffer),
          '/' if iter.peek() == Some(&'*') => skip_comment(iter),
          '/' if iter.peek() == Some(&'/') => skip_line_comment(iter),
          '(' => get_inside_paren(iter, c, ')', buffer),
          '[' => get_inside_paren(iter, c, ']', buffer),
          _ => buffer.push(c),
      }
  }
  
  /// Returns a CSS property name. Ends when encountering a `:` character.
  ///
  /// If the `:` character isn't found, returns `None`.
  ///
  /// If a `{` character is encountered, returns an error.
  fn parse_property_name(iter: &mut Peekable<Chars<'_>>) -> Result<Option<String>, String> {
      let mut content = String::new();
  
      while let Some(c) = iter.next() {
          match c {
              ':' => return Ok(Some(content.trim().to_owned())),
              '{' => return Err("Unexpected `{` in a `{}` block".to_owned()),
              '}' => break,
              _ => handle_common_chars(c, &mut content, iter),
          }
      }
      Ok(None)
  }
  
  /// Try to get the value of a CSS property (the `#fff` in `color: #fff`). It'll stop when it
  /// encounters a `{` or a `;` character.
  ///
  /// It returns the value string and a boolean set to `true` if the value is ended with a `}` because
  /// it means that the parent block is done and that we should notify the parent caller.
  fn parse_property_value(iter: &mut Peekable<Chars<'_>>) -> (String, bool) {
      let mut value = String::new();
      let mut out_block = false;
  
      while let Some(c) = iter.next() {
          match c {
              ';' => break,
              '}' => {
                  out_block = true;
                  break;
              }
              _ => handle_common_chars(c, &mut value, iter),
          }
      }
      (value.trim().to_owned(), out_block)
  }
  
  /// This is used to parse inside a CSS `{}` block. If we encounter a new `{` inside it, we consider
  /// it as a new block and therefore recurse into `parse_rules`.
  fn parse_rules(
      content: &str,
      selector: String,
      iter: &mut Peekable<Chars<'_>>,
      paths: &mut FxIndexMap<String, CssPath>,
  ) -> Result<(), String> {
      let mut rules = FxIndexMap::default();
      let mut children = FxIndexMap::default();
  
      loop {
          // If the parent isn't a "normal" CSS selector, we only expect sub-selectors and not CSS
          // properties.
          if selector.starts_with('@') {
              parse_selectors(content, iter, &mut children)?;
              break;
          }
          let rule = match parse_property_name(iter)? {
              Some(r) => {
                  if r.is_empty() {
                      return Err(format!("Found empty rule in selector `{selector}`"));
                  }
                  r
              }
              None => break,
          };
          let (value, out_block) = parse_property_value(iter);
          if value.is_empty() {
              return Err(format!("Found empty value for rule `{rule}` in selector `{selector}`"));
          }
          match rules.entry(rule) {
              IndexEntry::Occupied(mut o) => {
                  *o.get_mut() = value;
              }
              IndexEntry::Vacant(v) => {
                  v.insert(value);
              }
          }
          if out_block {
              break;
          }
      }
  
      match paths.entry(selector) {
          IndexEntry::Occupied(mut o) => {
              let v = o.get_mut();
              for (key, value) in rules.into_iter() {
                  v.rules.insert(key, value);
              }
              for (sel, child) in children.into_iter() {
                  v.children.insert(sel, child);
              }
          }
          IndexEntry::Vacant(v) => {
              v.insert(CssPath { rules, children });
          }
      }
      Ok(())
  }
  
  pub(crate) fn parse_selectors(
      content: &str,
      iter: &mut Peekable<Chars<'_>>,
      paths: &mut FxIndexMap<String, CssPath>,
  ) -> Result<(), String> {
      let mut selector = String::new();
  
      while let Some(c) = iter.next() {
          match c {
              '{' => {
                  if selector.trim().starts_with(":root[data-theme") {
                      selector = String::from(":root");
                  }
                  let s = minifier::css::minify(selector.trim()).map(|s| s.to_string())?;
                  parse_rules(content, s, iter, paths)?;
                  selector.clear();
              }
              '}' => break,
              ';' => selector.clear(), // We don't handle inline selectors like `@import`.
              _ => handle_common_chars(c, &mut selector, iter),
          }
      }
      Ok(())
  }
  
  /// The entry point to parse the CSS rules. Every time we encounter a `{`, we then parse the rules
  /// inside it.
  pub(crate) fn load_css_paths(content: &str) -> Result<FxIndexMap<String, CssPath>, String> {
      let mut iter = content.chars().peekable();
      let mut paths = FxIndexMap::default();
  
      parse_selectors(content, &mut iter, &mut paths)?;
      Ok(paths)
  }
  
  pub(crate) fn get_differences(
      origin: &FxIndexMap<String, CssPath>,
      against: &FxIndexMap<String, CssPath>,
      v: &mut Vec<String>,
  ) {
      for (selector, entry) in origin.iter() {
          match against.get(selector) {
              Some(a) => {
                  get_differences(&entry.children, &a.children, v);
                  if selector == ":root" {
                      // We need to check that all variables have been set.
                      for rule in entry.rules.keys() {
                          if !a.rules.contains_key(rule) {
                              v.push(format!("  Missing CSS variable `{rule}` in `:root`"));
                          }
                      }
                  }
              }
              None => v.push(format!("  Missing rule `{selector}`")),
          }
      }
  }
  
  pub(crate) fn test_theme_against<P: AsRef<Path>>(
      f: &P,
      origin: &FxIndexMap<String, CssPath>,
      dcx: DiagCtxtHandle<'_>,
  ) -> (bool, Vec<String>) {
      let against = match fs::read_to_string(f)
          .map_err(|e| e.to_string())
          .and_then(|data| load_css_paths(&data))
      {
          Ok(c) => c,
          Err(e) => {
              dcx.err(e);
              return (false, vec![]);
          }
      };
  
      let mut ret = vec![];
      get_differences(origin, &against, &mut ret);
      (true, ret)
  }
  