less-rs 0.0.0

A Rust implementation of the Less CSS preprocessor.
Documentation
use std::ops::{Deref, DerefMut};

use cssparser::{CowRcStr, ParseError};
use less_allocator::{Allocator, Vec};

use crate::{error::ParserError, rules::variable::Variable, values::variable::VariableReference};

/// Context for current parsing scope.
#[derive(Debug)]
pub(crate) struct Context<'i> {
  variables: Vec<'i, &'i Variable<'i>>,
}

#[derive(Debug)]
pub(crate) struct Contexts<'i>(Vec<'i, Context<'i>>);

impl<'i> Contexts<'i> {
  /// Try to resolve a variable reference in the current contexts.
  /// If the variable is not found, it returns an error.
  pub(crate) fn resolve_variable(
    &self,
    variable: &VariableReference,
  ) -> Result<&'i Variable<'i>, ()> {
    match self.0.iter().rev().find_map(|context| {
      context
        .variables
        .iter()
        .rev()
        .find(|v| v.name == variable.0)
    }) {
      Some(variable) => Ok(variable),
      None => Err(()),
    }
  }
}

impl<'a, 'i> Deref for Contexts<'i> {
  type Target = Vec<'i, Context<'i>>;

  fn deref(&self) -> &Self::Target {
    &self.0
  }
}

impl<'a, 'i> DerefMut for Contexts<'i> {
  fn deref_mut(&mut self) -> &mut Self::Target {
    &mut self.0
  }
}

impl<'i> Contexts<'i> {
  fn new(allocator: &'i Allocator) -> Self {
    Self(Vec::new_in(allocator))
  }
}

/// Error for the printer.
///
/// The printer will produce CSS code, which may contain errors.
/// But it will not tolerate any errors that is related to parsing the less code.
/// For example, less variables that are not found in the current context will be reported as errors.
#[derive(Debug)]
pub enum PrinterError<'i> {
  LessVariableParseError(ParseError<'i, ParserError<'i>>),
  LessRecursiveVariable(VariableReference<'i>),
  LessVariableUndefined(VariableReference<'i>),
  /// Failed to access a less property using '$'<ident-token> syntax.
  LessPropertyUndefined(CowRcStr<'i>),
  Error(std::fmt::Error),
}

impl From<std::fmt::Error> for PrinterError<'_> {
  fn from(value: std::fmt::Error) -> Self {
    PrinterError::Error(value)
  }
}

pub struct Printer<'a, 'i, W: std::fmt::Write> {
  pub(crate) allocator: &'i Allocator,
  dest: &'a mut W,
  /// Contexts for the current parsing scope.
  pub(crate) context: Contexts<'i>,
  /// Current column number in the output
  pub(crate) col: u32,
  /// Current line number in the output
  pub(crate) line: u32,
}

impl<'a, 'i, W: std::fmt::Write> Printer<'a, 'i, W> {
  pub fn new(allocator: &'i Allocator, dest: &'a mut W) -> Self {
    Self {
      allocator,
      dest,
      context: Contexts::new(allocator),

      col: 0,
      line: 0,
    }
  }

  pub fn with_context<F: FnOnce(&mut Printer<'a, 'i, W>) -> Result<(), PrinterError<'i>>>(
    &mut self,
    variables: Vec<&Variable>,
    f: F,
  ) -> Result<(), PrinterError<'i>> {
    // SAFETY: Variables are allocated in the allocator, which is of lifetime 'i.
    // `Vec` used to hold variables is not exact lifetime 'i, but it's valid for entire lifetime of the closure.
    let variables = unsafe {
      #[expect(clippy::missing_transmute_annotations)]
      std::mem::transmute(variables)
    };
    let context = Context { variables };
    self.context.push(context);
    f(self)?;
    self.context.pop();
    Ok(())
  }

  /// Write a string to the printer.
  ///
  /// This assumes the string does not contain any newlines.
  /// If the string contains newlines, it will not update the line number correctly.
  pub fn write_str(&mut self, s: &str) -> Result<(), PrinterError<'i>> {
    debug_assert!(!s.contains('\n'), "String contains newline: '{}'", s);
    self.col += s.len() as u32;
    self.dest.write_str(s)?;
    Ok(())
  }

  /// Write a character to the printer.
  ///
  /// If the character is a newline, it will update the line number and column number accordingly.
  pub fn write_char(&mut self, c: char) -> Result<(), PrinterError<'i>> {
    if c == '\n' {
      self.write_newline()?;
      return Ok(());
    }
    self.col += 1;
    self.dest.write_char(c)?;
    Ok(())
  }

  /// Write a newline to the printer.
  pub fn write_newline(&mut self) -> Result<(), PrinterError<'i>> {
    self.col += 1;
    self.line = 0;
    self.dest.write_char('\n')?;
    Ok(())
  }
}

impl<W: std::fmt::Write> std::fmt::Write for Printer<'_, '_, W> {
  /// Write a string to the printer.
  ///
  /// This assumes the string does not contain any newlines.
  /// If the string contains newlines, it will not update the line number correctly.
  fn write_str(&mut self, s: &str) -> std::fmt::Result {
    debug_assert!(!s.contains('\n'), "String contains newline: {}", s);
    self.col += s.len() as u32;
    self.dest.write_str(s)
  }
}