use std::ops::Deref; use mlua::{AnyUserData, ExternalError, FromLua, Lua, MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRef, Value}; use yazi_shared::{IntoOsStr, url::UrlCow}; use crate::{Urn, cached_field, deprecate}; pub type UrlRef = UserDataRef; pub struct Url { inner: yazi_shared::url::UrlBuf, v_name: Option, v_stem: Option, v_ext: Option, v_urn: Option, v_base: Option, v_parent: Option, v_domain: Option, } impl Deref for Url { type Target = yazi_shared::url::UrlBuf; fn deref(&self) -> &Self::Target { &self.inner } } impl AsRef for Url { fn as_ref(&self) -> &yazi_shared::url::UrlBuf { &self.inner } } impl From for yazi_shared::url::UrlBuf { fn from(value: Url) -> Self { value.inner } } impl<'a> From<&'a Url> for yazi_shared::url::Url<'a> { fn from(value: &'a Url) -> Self { value.as_url() } } impl<'a> From<&'a Url> for UrlCow<'a> { fn from(value: &'a Url) -> Self { UrlCow::Borrowed { loc: value.loc.as_loc(), scheme: value.scheme.as_ref().into() } } } impl From for yazi_shared::url::UrlBufCov { fn from(value: Url) -> Self { Self(value.inner) } } impl TryFrom<&[u8]> for Url { type Error = mlua::Error; fn try_from(value: &[u8]) -> mlua::Result { Ok(Self::new(UrlCow::try_from(value)?.into_owned())) } } impl Url { pub fn new(url: impl Into) -> Self { Self { inner: url.into(), v_name: None, v_stem: None, v_ext: None, v_urn: None, v_base: None, v_parent: None, v_domain: 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().as_ref())?, Value::UserData(ud) => Self::new(ud.borrow::()?.inner.clone()), _ => Err("Expected a string or a Url".into_lua_err())?, }) })?, ) } } impl FromLua for Url { fn from_lua(value: Value, _: &Lua) -> mlua::Result { Ok(match value { Value::UserData(ud) => ud.take()?, _ => Err("Expected a Url".into_lua_err())?, }) } } impl UserData for Url { fn add_fields>(fields: &mut F) { cached_field!(fields, name, |lua, me| { me.name().map(|s| lua.create_string(s.as_encoded_bytes())).transpose() }); cached_field!(fields, stem, |lua, me| { me.stem().map(|s| lua.create_string(s.as_encoded_bytes())).transpose() }); cached_field!(fields, ext, |lua, me| { me.ext().map(|s| lua.create_string(s.as_encoded_bytes())).transpose() }); cached_field!(fields, parent, |_, me| Ok(me.parent().map(Self::new))); cached_field!(fields, urn, |_, me| Ok(Urn::new(me.urn()))); cached_field!(fields, base, |_, me| Ok(me.base().map(Self::new))); cached_field!(fields, domain, |lua, me| { me.scheme.domain().map(|s| lua.create_string(s)).transpose() }); fields.add_field_method_get("frag", |lua, me| { deprecate!(lua, "`frag` property of Url is deprecated and renamed to `domain`, please use the new name instead, in your {}"); me.scheme.domain().map(|s| lua.create_string(s)).transpose() }); 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("join", |_, me, other: Value| { Ok(Self::new(match other { Value::String(s) => me.join(s.as_bytes().into_os_str()?), Value::UserData(ud) => { let url = ud.borrow::()?; if !me.scheme.covariant(&url.scheme) { return Err("cannot join Urls with different schemes".into_lua_err()); } me.join(&url.loc) } _ => Err("must be a string or Url".into_lua_err())?, })) }); methods.add_method("starts_with", |_, me, base: Value| { Ok(match base { Value::String(s) => me.loc.starts_with(s.as_bytes().into_os_str()?), Value::UserData(ud) => me.starts_with(&*ud.borrow::()?), _ => Err("must be a string or Url".into_lua_err())?, }) }); methods.add_method("ends_with", |_, me, child: Value| { Ok(match child { Value::String(s) => me.loc.ends_with(s.as_bytes().into_os_str()?), Value::UserData(ud) => me.ends_with(&*ud.borrow::()?), _ => Err("must be a string or Url".into_lua_err())?, }) }); methods.add_method("strip_prefix", |_, me, base: Value| { let path = match base { Value::String(s) => me.loc.strip_prefix(s.as_bytes().into_os_str()?).ok(), Value::UserData(ud) => me.strip_prefix(&*ud.borrow::()?).map(AsRef::as_ref), _ => Err("must be a string or Url".into_lua_err())?, }; Ok(path.map(Self::new)) // TODO: return `Path` instead of `Url` }); methods.add_function_mut("into_search", |_, (ud, domain): (AnyUserData, mlua::String)| { Ok(Self::new(ud.take::()?.inner.into_search(domain.to_str()?))) }); 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.os_str().as_encoded_bytes()) }); methods.add_meta_method(MetaMethod::Concat, |lua, lhs, rhs: mlua::String| { lua.create_string([lhs.os_str().as_encoded_bytes(), &rhs.as_bytes()].concat()) }); } }