use std::ops::Deref; use mlua::{AnyUserData, ExternalError, ExternalResult, IntoLua, Lua, MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRef, Value}; use yazi_codegen::FromLuaOwned; use yazi_fs::{FsHash64, FsHash128, FsUrl}; use yazi_shared::{path::{PathLike, StripPrefixError}, scheme::{SchemeCow, SchemeLike}, strand::{StrandLike, ToStrand}, url::{AsUrl, UrlCow, UrlLike}}; use crate::{Path, Scheme, cached_field}; pub type UrlRef = UserDataRef; const EXPECTED: &str = "expected a string, Url, or Path"; #[derive(FromLuaOwned)] pub struct Url { inner: yazi_shared::url::UrlBuf, v_path: Option, v_name: Option, v_stem: Option, v_ext: Option, v_urn: Option, v_base: Option, v_parent: Option, v_scheme: Option, v_domain: Option, v_cache: Option, } impl Deref for Url { type Target = yazi_shared::url::UrlBuf; fn deref(&self) -> &Self::Target { &self.inner } } impl AsUrl for Url { fn as_url(&self) -> yazi_shared::url::Url<'_> { self.inner.as_url() } } impl AsUrl for &Url { fn as_url(&self) -> yazi_shared::url::Url<'_> { self.inner.as_url() } } impl From for yazi_shared::url::UrlBuf { fn from(value: Url) -> Self { value.inner } } impl From for yazi_shared::url::UrlBufCov { fn from(value: Url) -> Self { Self(value.inner) } } impl From for yazi_shared::url::UrlCow<'_> { fn from(value: Url) -> Self { value.inner.into() } } impl TryFrom<&[u8]> for Url { type Error = mlua::Error; fn try_from(value: &[u8]) -> mlua::Result { Ok(Self::new(UrlCow::try_from(value)?)) } } impl Url { pub fn new(url: impl Into) -> Self { Self { inner: url.into(), v_path: None, v_name: None, v_stem: None, v_ext: None, v_urn: None, v_base: None, v_parent: None, v_scheme: None, v_domain: None, v_cache: None, } } pub fn install(lua: &Lua) -> mlua::Result<()> { lua.globals().raw_set( "Url", lua.create_function(|_, value: Value| { Ok(match value { Value::String(s) => Self::try_from(&*s.as_bytes())?, Value::UserData(ud) => { if let Ok(url) = ud.borrow::() { Self::new(&url.inner) } else if let Ok(path) = ud.borrow::() { Self::new(path.as_os().into_lua_err()?) } else { Err(EXPECTED.into_lua_err())? } } _ => Err(EXPECTED.into_lua_err())?, }) })?, ) } fn ends_with(&self, child: Value) -> mlua::Result { match child { Value::String(s) => self.try_ends_with(UrlCow::try_from(&*s.as_bytes())?).into_lua_err(), Value::UserData(ud) => self.try_ends_with(&*ud.borrow::()?).into_lua_err(), _ => Err("must be a string or Url".into_lua_err())?, } } fn hash(&self, long: Option) -> mlua::Result { Ok(if long.unwrap_or(false) { format!("{:x}", self.hash_u128()) } else { format!("{:x}", self.hash_u64()) }) } fn join(&self, lua: &Lua, other: Value) -> mlua::Result { match other { Value::String(s) => { let b = s.as_bytes(); let (scheme, path) = SchemeCow::parse(&b)?; if scheme.covariant(self.scheme()) { Self::new(self.try_join(path).into_lua_err()?).into_lua(lua) } else { Self::new(UrlCow::try_from((scheme, path))?).into_lua(lua) } } Value::UserData(ref ud) => { let url = ud.borrow::()?; if url.scheme().covariant(self.scheme()) { Self::new(self.try_join(url.loc()).into_lua_err()?).into_lua(lua) } else { Ok(other) } } _ => Err("must be a string or Url".into_lua_err())?, } } fn starts_with(&self, base: Value) -> mlua::Result { match base { Value::String(s) => self.try_starts_with(UrlCow::try_from(&*s.as_bytes())?).into_lua_err(), Value::UserData(ud) => self.try_starts_with(&*ud.borrow::()?).into_lua_err(), _ => Err("must be a string or Url".into_lua_err())?, } } fn strip_prefix(&self, base: Value) -> mlua::Result> { let strip = match base { Value::String(s) => self.try_strip_prefix(UrlCow::try_from(&*s.as_bytes())?), Value::UserData(ud) => self.try_strip_prefix(&*ud.borrow::()?), _ => Err("must be a string or Url".into_lua_err())?, }; Ok(match strip { Ok(p) => Some(Path::new(p)), Err(StripPrefixError::Exotic | StripPrefixError::NotPrefix) => None, Err(e @ StripPrefixError::WrongEncoding) => Err(e.into_lua_err())?, }) } } impl UserData for Url { fn add_fields>(fields: &mut F) { cached_field!(fields, path, |_, me| Ok(Path::new(me.loc()))); cached_field!(fields, name, |lua, me| { me.name().map(|s| lua.create_string(s.encoded_bytes())).transpose() }); cached_field!(fields, stem, |lua, me| { me.stem().map(|s| lua.create_string(s.encoded_bytes())).transpose() }); cached_field!(fields, ext, |lua, me| { me.ext().map(|s| lua.create_string(s.encoded_bytes())).transpose() }); cached_field!(fields, urn, |_, me| Ok(Path::new(me.urn()))); cached_field!(fields, base, |_, me| { Ok(Some(me.base()).filter(|u| !u.loc().is_empty()).map(Self::new)) }); cached_field!(fields, parent, |_, me| Ok(me.parent().map(Self::new))); cached_field!(fields, scheme, |_, me| Ok(Scheme::new(me.scheme()))); cached_field!(fields, domain, |lua, me| { me.scheme().domain().map(|s| lua.create_string(s)).transpose() }); cached_field!(fields, cache, |_, me| Ok(me.cache().map(Path::new))); fields.add_field_method_get("is_regular", |_, me| Ok(me.is_regular())); fields.add_field_method_get("is_search", |_, me| Ok(me.is_search())); fields.add_field_method_get("is_archive", |_, me| Ok(me.is_archive())); fields.add_field_method_get("is_absolute", |_, me| Ok(me.is_absolute())); fields.add_field_method_get("has_root", |_, me| Ok(me.has_root())); } fn add_methods>(methods: &mut M) { methods.add_method("ends_with", |_, me, child: Value| me.ends_with(child)); methods.add_method("hash", |_, me, long: Option| me.hash(long)); methods.add_method("join", |lua, me, other: Value| me.join(lua, other)); methods.add_method("starts_with", |_, me, base: Value| me.starts_with(base)); methods.add_method("strip_prefix", |_, me, base: Value| me.strip_prefix(base)); methods.add_function_mut("into_search", |_, (ud, domain): (AnyUserData, mlua::String)| { let url = ud.take::()?.inner.into_search(domain.to_str()?).into_lua_err()?; Ok(Self::new(url)) }); methods.add_meta_method(MetaMethod::Eq, |_, me, other: UrlRef| Ok(me.inner == other.inner)); methods.add_meta_method(MetaMethod::ToString, |lua, me, ()| { lua.create_string(me.to_strand().encoded_bytes()) }); methods.add_meta_method(MetaMethod::Concat, |lua, lhs, rhs: mlua::String| { lua.create_external_string([lhs.to_strand().encoded_bytes(), &rhs.as_bytes()].concat()) }); } }