takumi 1.7.0

Render UI component trees to images.
Documentation
use std::{borrow::Cow, collections::HashMap};

use cssparser::{Parser, ParserInput, Token};

use super::DeferredDeclaration;
use crate::layout::style::{ComputedStyle, CssInput};

pub(super) fn resolve_var_function(
  input: &mut Parser<'_, '_>,
  custom_properties: &HashMap<String, String>,
  stack: &mut Vec<String>,
) -> Option<String> {
  let property_name = input.expect_ident_cloned().ok()?;
  if !property_name.starts_with("--") {
    return None;
  }

  let fallback = if input.try_parse(Parser::expect_comma).is_ok() {
    let mut output = String::new();
    resolve_var_tokens_into(input, custom_properties, stack, &mut output)?;
    Some(output)
  } else {
    None
  };

  if input.next_including_whitespace_and_comments().is_ok() {
    return None;
  }

  if stack.iter().any(|entry| entry == property_name.as_ref()) {
    return fallback;
  }

  let resolved = if let Some(specified_value) = custom_properties.get(property_name.as_ref()) {
    stack.push(property_name.to_string());
    let resolved = resolve_var_references(specified_value, custom_properties, stack);
    stack.pop();
    resolved
  } else {
    None
  };

  resolved.or(fallback)
}

fn resolve_var_tokens_into(
  input: &mut Parser<'_, '_>,
  custom_properties: &HashMap<String, String>,
  stack: &mut Vec<String>,
  output: &mut String,
) -> Option<()> {
  while !input.is_exhausted() {
    let start = input.position();
    let token = input.next_including_whitespace_and_comments().ok()?;

    match token {
      Token::Function(name) if name.eq_ignore_ascii_case("var") => {
        output.push_str(
          &input
            .parse_nested_block(|input| {
              resolve_var_function(input, custom_properties, stack)
                .ok_or_else(|| input.new_error_for_next_token::<()>())
            })
            .ok()?,
        );
      }
      Token::Function(name) => {
        output.push_str(name);
        output.push('(');
        input
          .parse_nested_block(|input| {
            resolve_var_tokens_into(input, custom_properties, stack, output)
              .ok_or_else(|| input.new_error_for_next_token::<()>())
          })
          .ok()?;
        output.push(')');
      }
      Token::ParenthesisBlock => {
        output.push('(');
        input
          .parse_nested_block(|input| {
            resolve_var_tokens_into(input, custom_properties, stack, output)
              .ok_or_else(|| input.new_error_for_next_token::<()>())
          })
          .ok()?;
        output.push(')');
      }
      Token::SquareBracketBlock => {
        output.push('[');
        input
          .parse_nested_block(|input| {
            resolve_var_tokens_into(input, custom_properties, stack, output)
              .ok_or_else(|| input.new_error_for_next_token::<()>())
          })
          .ok()?;
        output.push(']');
      }
      Token::CurlyBracketBlock => {
        output.push('{');
        input
          .parse_nested_block(|input| {
            resolve_var_tokens_into(input, custom_properties, stack, output)
              .ok_or_else(|| input.new_error_for_next_token::<()>())
          })
          .ok()?;
        output.push('}');
      }
      _ => output.push_str(input.slice_from(start)),
    }
  }

  Some(())
}

pub(crate) fn resolve_var_references(
  specified_value: &str,
  custom_properties: &HashMap<String, String>,
  stack: &mut Vec<String>,
) -> Option<String> {
  let mut parser_input = ParserInput::new(specified_value);
  let mut parser = Parser::new(&mut parser_input);
  let mut output = String::with_capacity(specified_value.len());
  resolve_var_tokens_into(&mut parser, custom_properties, stack, &mut output)?;
  Some(output)
}

pub(super) fn apply_deferred_declaration(
  style: &mut ComputedStyle,
  parent: Option<&ComputedStyle>,
  deferred: &DeferredDeclaration,
) {
  let Some(resolved_value) = resolve_var_references(
    &deferred.specified_value,
    &style.custom_properties,
    &mut Vec::new(),
  ) else {
    return;
  };

  let declarations = deferred
    .property
    .parse_css_input_declarations(CssInput::Str(Cow::Owned(resolved_value)))
    .ok();

  let Some(declarations) = declarations else {
    return;
  };

  for declaration in declarations {
    match parent {
      Some(parent) => declaration.apply_with_parent(style, parent),
      None => declaration.apply_to_computed(style),
    }
  }
}