diff --git a/Cargo.lock b/Cargo.lock index 8ff4e5f..e2bb8e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1522,6 +1522,7 @@ dependencies = [ "regex", "reqwest", "rustyline", + "self_cell", "serde", "serde_json", "serde_yaml", @@ -2423,6 +2424,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "self_cell" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58bf37232d3bb9a2c4e641ca2a11d83b5062066f88df7fed36c28772046d65ba" + [[package]] name = "semver" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index 9e35a0a..8199082 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,6 @@ cli = [ "dep:env_logger", "dep:clap", "dep:include_dir", - "dep:regex", "dep:rustyline", "dep:zip_next", ] @@ -76,7 +75,9 @@ path-clean = "1.0" pathdiff = "0.2" pin-project = "1.0" urlencoding = "2.1" -bstr = "1.9.1" +bstr = "1.9" +regex = "1.10" +self_cell = "1.0" ### RUNTIME @@ -133,10 +134,6 @@ env_logger = { optional = true, version = "0.11" } itertools = "0.12" clap = { optional = true, version = "4.1", features = ["derive"] } include_dir = { optional = true, version = "0.7", features = ["glob"] } -regex = { optional = true, version = "1.7", default-features = false, features = [ - "std", - "unicode-perl", -] } rustyline = { optional = true, version = "14.0" } zip_next = { optional = true, version = "1.1" } diff --git a/src/lune/builtins/regex/captures.rs b/src/lune/builtins/regex/captures.rs new file mode 100644 index 0000000..4dc67da --- /dev/null +++ b/src/lune/builtins/regex/captures.rs @@ -0,0 +1,76 @@ +use std::sync::Arc; + +use mlua::prelude::*; +use regex::{Captures, Regex}; +use self_cell::self_cell; + +use super::matches::LuaMatch; + +type OptionalCaptures<'a> = Option>; + +self_cell! { + struct LuaCapturesInner { + owner: Arc, + #[covariant] + dependent: OptionalCaptures, + } +} + +pub struct LuaCaptures { + inner: LuaCapturesInner, +} + +impl LuaCaptures { + pub fn new(pattern: &Regex, text: String) -> Self { + Self { + inner: LuaCapturesInner::new(Arc::from(text), |owned| pattern.captures(owned.as_str())), + } + } + + fn captures(&self) -> &Captures { + self.inner + .borrow_dependent() + .as_ref() + .expect("None captures should not be used") + } + + fn num_captures(&self) -> usize { + // NOTE: Here we exclude the match for the entire regex + // pattern, only counting the named and numbered captures + self.captures().len() - 1 + } + + fn text(&self) -> Arc { + Arc::clone(self.inner.borrow_owner()) + } +} + +impl LuaUserData for LuaCaptures { + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_method("get", |_, this, n: usize| { + Ok(this + .captures() + .get(n) + .map(|m| LuaMatch::new(this.text(), m))) + }); + + methods.add_method("group", |_, this, group: String| { + Ok(this + .captures() + .name(&group) + .map(|m| LuaMatch::new(this.text(), m))) + }); + + methods.add_method("format", |_, this, format: String| { + let mut new = String::new(); + this.captures().expand(&format, &mut new); + Ok(new) + }); + + methods.add_meta_method(LuaMetaMethod::Type, |_, _, ()| Ok("RegexCaptures")); + methods.add_meta_method(LuaMetaMethod::Len, |_, this, ()| Ok(this.num_captures())); + methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| { + Ok(format!("RegexCaptures({})", this.num_captures())) + }); + } +} diff --git a/src/lune/builtins/regex/matches.rs b/src/lune/builtins/regex/matches.rs new file mode 100644 index 0000000..60348e8 --- /dev/null +++ b/src/lune/builtins/regex/matches.rs @@ -0,0 +1,48 @@ +use std::{ops::Range, sync::Arc}; + +use mlua::prelude::*; +use regex::Match; + +pub struct LuaMatch { + text: Arc, + start: usize, + end: usize, +} + +impl LuaMatch { + pub fn new(text: Arc, matched: Match) -> Self { + Self { + text, + start: matched.start(), + end: matched.end(), + } + } + + fn range(&self) -> Range { + self.start..self.end + } + + fn slice(&self) -> &str { + &self.text[self.range()] + } +} + +impl LuaUserData for LuaMatch { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + // NOTE: Strings are 0 based in Rust but 1 based in Luau, and end of range in Rust is exclusive + fields.add_field_method_get("start", |_, this| Ok(this.start.saturating_add(1))); + fields.add_field_method_get("finish", |_, this| Ok(this.end)); + fields.add_field_method_get("len", |_, this| Ok(this.range().len())); + fields.add_field_method_get("text", |_, this| Ok(this.slice().to_string())); + } + + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_method("isEmpty", |_, this, ()| Ok(this.range().is_empty())); + + methods.add_meta_method(LuaMetaMethod::Type, |_, _, ()| Ok("RegexMatch")); + methods.add_meta_method(LuaMetaMethod::Len, |_, this, ()| Ok(this.range().len())); + methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| { + Ok(format!("RegexMatch({})", this.slice())) + }); + } +} diff --git a/src/lune/builtins/regex/mod.rs b/src/lune/builtins/regex/mod.rs index 471062c..bb674c2 100644 --- a/src/lune/builtins/regex/mod.rs +++ b/src/lune/builtins/regex/mod.rs @@ -1,7 +1,21 @@ +#![allow(clippy::module_inception)] + use mlua::prelude::*; use crate::lune::util::TableBuilder; +mod captures; +mod matches; +mod regex; + +use self::regex::LuaRegex; + pub fn create(lua: &Lua) -> LuaResult { - TableBuilder::new(lua)?.build_readonly() + TableBuilder::new(lua)? + .with_function("new", new_regex)? + .build_readonly() +} + +fn new_regex(_: &Lua, pattern: String) -> LuaResult { + LuaRegex::new(pattern) } diff --git a/src/lune/builtins/regex/regex.rs b/src/lune/builtins/regex/regex.rs new file mode 100644 index 0000000..b255caa --- /dev/null +++ b/src/lune/builtins/regex/regex.rs @@ -0,0 +1,66 @@ +use std::sync::Arc; + +use mlua::prelude::*; +use regex::Regex; + +use super::{captures::LuaCaptures, matches::LuaMatch}; + +pub struct LuaRegex { + inner: Regex, +} + +impl LuaRegex { + pub fn new(pattern: String) -> LuaResult { + Regex::new(&pattern) + .map(|inner| Self { inner }) + .map_err(LuaError::external) + } +} + +impl LuaUserData for LuaRegex { + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_method("isMatch", |_, this, text: String| { + Ok(this.inner.is_match(&text)) + }); + + methods.add_method("find", |_, this, text: String| { + let arc = Arc::new(text); + Ok(this + .inner + .find(&arc) + .map(|m| LuaMatch::new(Arc::clone(&arc), m))) + }); + + methods.add_method("captures", |_, this, text: String| { + Ok(LuaCaptures::new(&this.inner, text)) + }); + + methods.add_method("split", |_, this, text: String| { + Ok(this + .inner + .split(&text) + .map(|s| s.to_string()) + .collect::>()) + }); + + // TODO: Determine whether it's desirable and / or feasible to support + // using a function or table for `replace` like in the lua string library + methods.add_method( + "replace", + |_, this, (haystack, replacer): (String, String)| { + Ok(this.inner.replace(&haystack, replacer).to_string()) + }, + ); + methods.add_method( + "replaceAll", + |_, this, (haystack, replacer): (String, String)| { + Ok(this.inner.replace_all(&haystack, replacer).to_string()) + }, + ); + + methods.add_meta_method(LuaMetaMethod::Type, |_, _, ()| Ok("Regex")); + methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| { + Ok(format!("Regex({})", this.inner.as_str())) + }); + } +}