rspack_swc_plugin_import 0.7.10

babel-plugin-import rewritten in Rust
Documentation
use std::{error::Error, fmt, sync::Arc};

use rustc_hash::FxHashMap as HashMap;

type HelperFn = Arc<dyn Fn(&str) -> String + Send + Sync>;

#[derive(Clone, Debug)]
pub struct Template<'a> {
  segments: Vec<Segment<'a>>,
}

#[derive(Clone, Debug)]
enum Segment<'a> {
  Text(&'a str),
  Value(Value<'a>),
}

#[derive(Clone, Debug)]
enum Value<'a> {
  Variable(&'a str),
  Helper { helper: &'a str, argument: &'a str },
}

#[derive(Debug, Clone)]
pub enum TemplateError {
  UnclosedTag { value: String },
  InvalidTemplateString { value: String },
  UnknownHelper { name: String },
  MissingValue { name: String },
  TemplateNotFound { name: String },
}

impl fmt::Display for TemplateError {
  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    match self {
      TemplateError::UnclosedTag { value } => {
        write!(f, "unclosed tag in template string \"{value}\"")
      }
      TemplateError::InvalidTemplateString { value } => {
        write!(f, "invalid template string `{{{{ {value} }}}}`")
      }
      TemplateError::UnknownHelper { name } => write!(f, "invalid helper \"{name}\""),
      TemplateError::MissingValue { name } => write!(f, "missing value for `{name}`"),
      TemplateError::TemplateNotFound { name } => write!(f, "template `{name}` not found"),
    }
  }
}

impl Error for TemplateError {}

impl<'a> Template<'a> {
  pub fn parse(input: &'a str) -> Result<Self, TemplateError> {
    let mut segments = Vec::new();
    let mut cursor = 0usize;
    let s = input;

    while let Some(start) = find_subsequence(s, cursor, "{{") {
      if start > cursor {
        segments.push(Segment::Text(&input[cursor..start]));
      }

      let tag_start = start + 2;
      let end = find_subsequence(s, tag_start, "}}").ok_or(TemplateError::UnclosedTag {
        value: input.to_string(),
      })?;

      let expression = input[tag_start..end].trim();
      if expression.is_empty() {
        return Err(TemplateError::InvalidTemplateString {
          value: expression.to_string(),
        });
      }

      let segment = Value::parse(expression)?;
      segments.push(Segment::Value(segment));

      cursor = end + 2;
    }

    if cursor < input.len() {
      segments.push(Segment::Text(&input[cursor..]));
    }

    Ok(Self { segments })
  }

  fn render(
    &self,
    engine: &TemplateEngine<'a>,
    ctx: &HashMap<&'static str, String>,
  ) -> Result<String, TemplateError> {
    let mut output = String::new();
    for segment in &self.segments {
      match segment {
        Segment::Text(text) => output.push_str(text),
        Segment::Value(value) => match value {
          Value::Variable(name) => {
            let value = ctx.get(name).ok_or_else(|| TemplateError::MissingValue {
              name: (*name).to_string(),
            })?;
            output.push_str(value);
          }
          Value::Helper { helper, argument } => {
            let value = ctx
              .get(argument)
              .ok_or_else(|| TemplateError::MissingValue {
                name: (*argument).to_string(),
              })?;
            let helper_fn =
              engine
                .helpers
                .get(helper)
                .ok_or_else(|| TemplateError::UnknownHelper {
                  name: (*helper).to_string(),
                })?;
            let rendered = helper_fn(value);
            output.push_str(&rendered);
          }
        },
      }
    }
    Ok(output)
  }
}

impl<'a> Value<'a> {
  fn parse(expression: &'a str) -> Result<Value<'a>, TemplateError> {
    let mut parts = expression.split_whitespace();
    let first = parts
      .next()
      .ok_or_else(|| TemplateError::InvalidTemplateString {
        value: expression.to_string(),
      })?;
    let second = parts.next();

    if parts.next().is_some() {
      return Err(TemplateError::InvalidTemplateString {
        value: expression.to_string(),
      });
    }

    if let Some(arg) = second {
      Ok(Value::Helper {
        helper: first,
        argument: arg,
      })
    } else {
      Ok(Value::Variable(first))
    }
  }
}

#[derive(Clone, Default)]
pub struct TemplateEngine<'a> {
  helpers: HashMap<&'static str, HelperFn>,
  templates: HashMap<String, Template<'a>>,
}

impl<'a> TemplateEngine<'a> {
  pub fn new() -> Self {
    Self::default()
  }

  pub fn register_helper<F>(&mut self, name: &'static str, helper: F)
  where
    F: Fn(&str) -> String + Send + Sync + 'static,
  {
    self.helpers.insert(name, Arc::new(helper));
  }

  pub fn register_template(&mut self, name: String, template: Template<'a>) {
    self.templates.insert(name, template);
  }

  pub fn render(
    &self,
    name: &str,
    ctx: &HashMap<&'static str, String>,
  ) -> Result<String, TemplateError> {
    let template = self
      .templates
      .get(name)
      .ok_or_else(|| TemplateError::TemplateNotFound {
        name: name.to_string(),
      })?;
    template.render(self, ctx)
  }
}

fn find_subsequence(haystack: &str, from: usize, needle: &str) -> Option<usize> {
  haystack[from..].find(needle).map(|off| off + from)
}

#[cfg(test)]
mod tests {
  use heck::ToKebabCase;

  use super::*;

  fn simple_ctx(value: &str) -> HashMap<&'static str, String> {
    let mut map = HashMap::default();
    map.insert("member", value.to_string());
    map
  }

  #[test]
  fn render_plain_and_variable() {
    let template = Template::parse("foo {{ member }} bar").unwrap();
    let mut engine = TemplateEngine::new();
    engine.register_template("test".to_string(), template);

    let rendered = engine.render("test", &simple_ctx("MyButton")).unwrap();
    assert_eq!(rendered, "foo MyButton bar");
  }

  #[test]
  fn render_with_helper() {
    let template = Template::parse("foo/{{ kebabCase member }}").unwrap();
    let mut engine = TemplateEngine::new();
    engine.register_helper("kebabCase", |value| value.to_kebab_case());
    engine.register_template("test".to_string(), template);

    let rendered = engine.render("test", &simple_ctx("MyButton")).unwrap();
    assert_eq!(rendered, "foo/my-button");
  }

  #[test]
  fn render_with_extra_whitespace() {
    let template = Template::parse("foo/{{    kebabCase   member   }}").unwrap();
    let mut engine = TemplateEngine::new();
    engine.register_helper("kebabCase", |value| value.to_kebab_case());
    engine.register_template("test".to_string(), template);

    let rendered = engine.render("test", &simple_ctx("MyButton")).unwrap();
    assert_eq!(rendered, "foo/my-button");
  }

  #[test]
  fn render_variable_with_padding() {
    let template = Template::parse("{{    member   }}").unwrap();
    let mut engine = TemplateEngine::new();
    engine.register_template("test".to_string(), template);

    let rendered = engine.render("test", &simple_ctx("Button")).unwrap();
    assert_eq!(rendered, "Button");
  }

  #[test]
  fn error_on_missing_value() {
    let template = Template::parse("{{ member }}").unwrap();
    let mut engine = TemplateEngine::new();
    engine.register_template("test".to_string(), template);

    let result = engine.render("test", &HashMap::default());
    assert!(matches!(result, Err(TemplateError::MissingValue { .. })));
  }

  #[test]
  fn error_on_unknown_helper() {
    let template = Template::parse("{{ snakeCase member }}").unwrap();
    let mut engine = TemplateEngine::new();
    engine.register_template("test".to_string(), template);

    let result = engine.render("test", &simple_ctx("MyButton"));
    assert!(matches!(result, Err(TemplateError::UnknownHelper { .. })));
  }

  #[test]
  fn error_on_unclosed_tag() {
    let result = Template::parse("foo {{ member");
    assert!(matches!(result, Err(TemplateError::UnclosedTag { .. })));
  }

  #[test]
  fn error_on_invalid_expression() {
    let result = Template::parse("{{ }}");
    assert!(matches!(
      result,
      Err(TemplateError::InvalidTemplateString { .. })
    ));
  }
}