use std::convert::Infallible; use std::io; use ::url::{Position, Url, form_urlencoded, quirks}; use avril_core::{Extension, Op}; fn parts(url: Url) -> Vec { // Blob origins inherit only HTTP(S), not every scheme accepted by rust-url's origin API. let origin = if url.scheme() == "blob" { Url::parse(url.path()) .ok() .filter(|inner| matches!(inner.scheme(), "http" | "https")) .map_or_else(|| "null".into(), |inner| quirks::origin(&inner)) } else { quirks::origin(&url) }; // Order is shared with the component table in url.js. vec![ quirks::href(&url).into(), origin, quirks::protocol(&url).into(), quirks::username(&url).into(), quirks::password(&url).into(), quirks::host(&url).into(), quirks::hostname(&url).into(), quirks::port(&url).into(), quirks::pathname(&url).into(), quirks::search(&url).into(), quirks::hash(&url).into(), ] } pub fn init() -> Extension { Extension { name: "ext:url", dependencies: &["ext:web"], ops: vec![ Op::new("op_url_parse", |(input, base): (String, Option)| { let parsed = (|| { let base = base.as_deref().map(Url::parse).transpose().ok()?; Url::options() .base_url(base.as_ref()) .parse(&input) .ok() .map(parts) })(); Ok::<_, Infallible>(parsed) }), Op::new( "op_url_set", |(href, component, value): (String, String, String)| { let mut url = Url::parse(&href).map_err(io::Error::other)?; let original = url.clone(); match component.as_str() { "href" => { if quirks::set_href(&mut url, &value).is_err() { return Ok(None); } } "protocol" => { let _ = quirks::set_protocol(&mut url, &value); } "username" => { let _ = quirks::set_username(&mut url, &value); } "password" => { let _ = quirks::set_password(&mut url, &value); } "host" => { let _ = quirks::set_host(&mut url, &value); } "hostname" => { let _ = quirks::set_hostname(&mut url, &value); } "port" => { let _ = quirks::set_port(&mut url, &value); } "pathname" => quirks::set_pathname(&mut url, &value), "search" => quirks::set_search(&mut url, &value), "hash" => quirks::set_hash(&mut url, &value), _ => { return Err(io::Error::new( io::ErrorKind::InvalidInput, "Unknown URL component", )); } } // rust-url setters can omit the /. prefix that disambiguates a hostless // path. let href = if url.host_str().is_none() && !url.cannot_be_a_base() && url.path().starts_with("//") { format!("{}:/.{}", url.scheme(), &url[Position::BeforePath..]) } else { url.to_string() }; let updated = parts(url); let reparsed = Url::parse(&href).ok().map(parts); // Never retain components that change meaning when the next op reparses href. Ok(Some(match reparsed { Some(parsed) if parsed[1..] == updated[1..] => parsed, _ => parts(original), })) }, ), Op::new("op_url_search_parse", |query: String| { Ok::<_, Infallible>( form_urlencoded::parse(query.as_bytes()) .into_owned() .collect::>(), ) }), Op::new("op_url_search_serialize", |pairs: Vec<(String, String)>| { Ok::<_, Infallible>( form_urlencoded::Serializer::new(String::new()) .extend_pairs(pairs) .finish(), ) }), ], modules: &[ ( "ext:url/main.js", include_str!(concat!(env!("OUT_DIR"), "/main.js")), ), ( "ext:url/url.js", include_str!(concat!(env!("OUT_DIR"), "/url.js")), ), ( "ext:url/search_params.js", include_str!(concat!(env!("OUT_DIR"), "/search_params.js")), ), ( "ext:url/state.js", include_str!(concat!(env!("OUT_DIR"), "/state.js")), ), ], entry_point: Some("ext:url/main.js"), } }