mirror of https://github.com/boa-dev/boa.git
Browse Source
* Add a URL class (with caveats) Some methods are NOT currently supported (some don' make sense outside of a browser context). They are still implemented but will throw a JavaScript Error. Supported methods should follow the specification perfectly. * Adding tests and using url::quirks for simpler getters/setters * clippies * Address commentspull/4018/head
Hans Larsen
1 month ago
committed by
GitHub
8 changed files with 426 additions and 10 deletions
@ -0,0 +1,236 @@
|
||||
//! Boa's implementation of JavaScript's `URL` Web API class.
|
||||
//!
|
||||
//! The `URL` class can be instantiated from any global object.
|
||||
//! This relies on the `url` feature.
|
||||
//!
|
||||
//! More information:
|
||||
//! - [MDN documentation][mdn]
|
||||
//! - [WHATWG `URL` specification][spec]
|
||||
//!
|
||||
//! [spec]: https://url.spec.whatwg.org/
|
||||
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/URL
|
||||
#![cfg(feature = "url")] |
||||
|
||||
#[cfg(test)] |
||||
mod tests; |
||||
|
||||
use boa_engine::value::Convert; |
||||
use boa_engine::{ |
||||
js_error, js_string, Context, Finalize, JsData, JsResult, JsString, JsValue, Trace, |
||||
}; |
||||
use boa_interop::{js_class, IntoJsFunctionCopied, JsClass}; |
||||
use std::fmt::Display; |
||||
|
||||
/// The `URL` class represents a (properly parsed) Uniform Resource Locator.
|
||||
#[derive(Debug, Clone, JsData, Trace, Finalize)] |
||||
#[boa_gc(unsafe_no_drop)] |
||||
pub struct Url(#[unsafe_ignore_trace] url::Url); |
||||
|
||||
impl Url { |
||||
/// Register the `URL` class into the realm.
|
||||
///
|
||||
/// # Errors
|
||||
/// This will error if the context or realm cannot register the class.
|
||||
pub fn register(context: &mut Context) -> JsResult<()> { |
||||
context.register_global_class::<Self>()?; |
||||
Ok(()) |
||||
} |
||||
|
||||
/// Create a new `URL` object. Meant to be called from the JavaScript constructor.
|
||||
///
|
||||
/// # Errors
|
||||
/// Any errors that might occur during URL parsing.
|
||||
fn js_new(Convert(ref url): Convert<String>, base: &Option<Convert<String>>) -> JsResult<Self> { |
||||
if let Some(Convert(base)) = base { |
||||
let base_url = url::Url::parse(base) |
||||
.map_err(|e| js_error!(TypeError: "Failed to parse base URL: {}", e))?; |
||||
if base_url.cannot_be_a_base() { |
||||
return Err(js_error!(TypeError: "Base URL {} cannot be a base", base)); |
||||
} |
||||
|
||||
let url = base_url |
||||
.join(url) |
||||
.map_err(|e| js_error!(TypeError: "Failed to parse URL: {}", e))?; |
||||
Ok(Self(url)) |
||||
} else { |
||||
let url = url::Url::parse(url) |
||||
.map_err(|e| js_error!(TypeError: "Failed to parse URL: {}", e))?; |
||||
Ok(Self(url)) |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl Display for Url { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
write!(f, "{}", self.0) |
||||
} |
||||
} |
||||
|
||||
impl From<url::Url> for Url { |
||||
fn from(url: url::Url) -> Self { |
||||
Self(url) |
||||
} |
||||
} |
||||
|
||||
impl From<Url> for url::Url { |
||||
fn from(url: Url) -> url::Url { |
||||
url.0 |
||||
} |
||||
} |
||||
|
||||
js_class! { |
||||
class Url as "URL" { |
||||
property hash { |
||||
fn get(this: JsClass<Url>) -> JsString { |
||||
JsString::from(url::quirks::hash(&this.borrow().0)) |
||||
} |
||||
|
||||
fn set(this: JsClass<Url>, value: Convert<String>) { |
||||
url::quirks::set_hash(&mut this.borrow_mut().0, &value.0); |
||||
} |
||||
} |
||||
|
||||
property hostname { |
||||
fn get(this: JsClass<Url>) -> JsString { |
||||
JsString::from(url::quirks::hostname(&this.borrow().0)) |
||||
} |
||||
|
||||
fn set(this: JsClass<Url>, value: Convert<String>) { |
||||
let _ = url::quirks::set_hostname(&mut this.borrow_mut().0, &value.0); |
||||
} |
||||
} |
||||
|
||||
property host { |
||||
fn get(this: JsClass<Url>) -> JsString { |
||||
JsString::from(url::quirks::host(&this.borrow().0)) |
||||
} |
||||
|
||||
fn set(this: JsClass<Url>, value: Convert<String>) { |
||||
let _ = url::quirks::set_host(&mut this.borrow_mut().0, &value.0); |
||||
} |
||||
} |
||||
|
||||
property href { |
||||
fn get(this: JsClass<Url>) -> JsString { |
||||
JsString::from(url::quirks::href(&this.borrow().0)) |
||||
} |
||||
|
||||
fn set(this: JsClass<Url>, value: Convert<String>) -> JsResult<()> { |
||||
url::quirks::set_href(&mut this.borrow_mut().0, &value.0) |
||||
.map_err(|e| js_error!(TypeError: "Failed to set href: {}", e)) |
||||
} |
||||
} |
||||
|
||||
property origin { |
||||
fn get(this: JsClass<Url>) -> JsString { |
||||
JsString::from(url::quirks::origin(&this.borrow().0)) |
||||
} |
||||
} |
||||
|
||||
property password { |
||||
fn get(this: JsClass<Url>) -> JsString { |
||||
JsString::from(url::quirks::password(&this.borrow().0)) |
||||
} |
||||
|
||||
fn set(this: JsClass<Url>, value: Convert<String>) { |
||||
let _ = url::quirks::set_password(&mut this.borrow_mut().0, &value.0); |
||||
} |
||||
} |
||||
|
||||
property pathname { |
||||
fn get(this: JsClass<Url>) -> JsString { |
||||
JsString::from(url::quirks::pathname(&this.borrow().0)) |
||||
} |
||||
|
||||
fn set(this: JsClass<Url>, value: Convert<String>) { |
||||
let () = url::quirks::set_pathname(&mut this.borrow_mut().0, &value.0); |
||||
} |
||||
} |
||||
|
||||
property port { |
||||
fn get(this: JsClass<Url>) -> JsString { |
||||
JsString::from(url::quirks::port(&this.borrow().0)) |
||||
} |
||||
|
||||
fn set(this: JsClass<Url>, value: Convert<JsString>) { |
||||
let _ = url::quirks::set_port(&mut this.borrow_mut().0, &value.0.to_std_string_lossy()); |
||||
} |
||||
} |
||||
|
||||
property protocol { |
||||
fn get(this: JsClass<Url>) -> JsString { |
||||
JsString::from(url::quirks::protocol(&this.borrow().0)) |
||||
} |
||||
|
||||
fn set(this: JsClass<Url>, value: Convert<String>) { |
||||
let _ = url::quirks::set_protocol(&mut this.borrow_mut().0, &value.0); |
||||
} |
||||
} |
||||
|
||||
property search { |
||||
fn get(this: JsClass<Url>) -> JsString { |
||||
JsString::from(url::quirks::search(&this.borrow().0)) |
||||
} |
||||
|
||||
fn set(this: JsClass<Url>, value: Convert<String>) { |
||||
url::quirks::set_search(&mut this.borrow_mut().0, &value.0); |
||||
} |
||||
} |
||||
|
||||
property search_params as "searchParams" { |
||||
fn get() -> JsResult<()> { |
||||
Err(js_error!(Error: "URL.searchParams is not implemented")) |
||||
} |
||||
} |
||||
|
||||
property username { |
||||
fn get(this: JsClass<Url>) -> JsString { |
||||
JsString::from(this.borrow().0.username()) |
||||
} |
||||
|
||||
fn set(this: JsClass<Url>, value: Convert<String>) { |
||||
let _ = this.borrow_mut().0.set_username(&value.0); |
||||
} |
||||
} |
||||
|
||||
constructor(url: Convert<String>, base: Option<Convert<String>>) { |
||||
Self::js_new(url, &base) |
||||
} |
||||
|
||||
init(class: &mut ClassBuilder) -> JsResult<()> { |
||||
let create_object_url = (|| -> JsResult<()> { |
||||
Err(js_error!(Error: "URL.createObjectURL is not implemented")) |
||||
}) |
||||
.into_js_function_copied(class.context()); |
||||
let can_parse = (|url: Convert<String>, base: Option<Convert<String>>| { |
||||
Url::js_new(url, &base).is_ok() |
||||
}) |
||||
.into_js_function_copied(class.context()); |
||||
let parse = (|url: Convert<String>, base: Option<Convert<String>>, context: &mut Context| { |
||||
Url::js_new(url, &base) |
||||
.map_or(Ok(JsValue::null()), |u| Url::from_data(u, context).map(JsValue::from)) |
||||
}) |
||||
.into_js_function_copied(class.context()); |
||||
let revoke_object_url = (|| -> JsResult<()> { |
||||
Err(js_error!(Error: "URL.revokeObjectURL is not implemented")) |
||||
}) |
||||
.into_js_function_copied(class.context()); |
||||
|
||||
class |
||||
.static_method(js_string!("createObjectURL"), 1, create_object_url) |
||||
.static_method(js_string!("canParse"), 2, can_parse) |
||||
.static_method(js_string!("parse"), 2, parse) |
||||
.static_method(js_string!("revokeObjectUrl"), 1, revoke_object_url); |
||||
|
||||
Ok(()) |
||||
} |
||||
|
||||
fn to_string as "toString"(this: JsClass<Url>) -> JsString { |
||||
JsString::from(format!("{}", this.borrow().0)) |
||||
} |
||||
|
||||
fn to_json as "toJSON"(this: JsClass<Url>) -> JsString { |
||||
JsString::from(format!("{}", this.borrow().0)) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,113 @@
|
||||
use crate::test::{run_test_actions, TestAction}; |
||||
|
||||
const TEST_HARNESS: &str = r#" |
||||
function assert(condition, message) { |
||||
if (!condition) { |
||||
if (!message) { |
||||
message = "Assertion failed"; |
||||
} |
||||
throw new Error(message); |
||||
} |
||||
} |
||||
|
||||
function assert_eq(a, b, message) { |
||||
if (a !== b) { |
||||
throw new Error(`${message} (${JSON.stringify(a)} !== ${JSON.stringify(b)})`); |
||||
} |
||||
} |
||||
"#; |
||||
|
||||
#[test] |
||||
fn url_basic() { |
||||
run_test_actions([ |
||||
TestAction::run(TEST_HARNESS), |
||||
TestAction::run( |
||||
r##" |
||||
url = new URL("https://example.com:8080/path/to/resource?query#fragment"); |
||||
assert(url instanceof URL); |
||||
assert_eq(url.href, "https://example.com:8080/path/to/resource?query#fragment"); |
||||
assert_eq(url.protocol, "https:"); |
||||
assert_eq(url.host, "example.com:8080"); |
||||
assert_eq(url.hostname, "example.com"); |
||||
assert_eq(url.port, "8080"); |
||||
assert_eq(url.pathname, "/path/to/resource"); |
||||
assert_eq(url.search, "?query"); |
||||
assert_eq(url.hash, "#fragment"); |
||||
"##, |
||||
), |
||||
]); |
||||
} |
||||
|
||||
#[test] |
||||
fn url_base() { |
||||
run_test_actions([ |
||||
TestAction::run(TEST_HARNESS), |
||||
TestAction::run( |
||||
r##" |
||||
url = new URL("https://example.com:8080/path/to/resource?query#fragment", "http://example.org/"); |
||||
assert_eq(url.href, "https://example.com:8080/path/to/resource?query#fragment"); |
||||
assert_eq(url.protocol, "https:"); |
||||
assert_eq(url.host, "example.com:8080"); |
||||
assert_eq(url.hostname, "example.com"); |
||||
assert_eq(url.port, "8080"); |
||||
assert_eq(url.pathname, "/path/to/resource"); |
||||
assert_eq(url.search, "?query"); |
||||
assert_eq(url.hash, "#fragment"); |
||||
"##, |
||||
), |
||||
TestAction::run( |
||||
r##" |
||||
url = new URL("/path/to/resource?query#fragment", "http://example.org/"); |
||||
assert_eq(url.href, "http://example.org/path/to/resource?query#fragment"); |
||||
assert_eq(url.protocol, "http:"); |
||||
assert_eq(url.host, "example.org"); |
||||
assert_eq(url.hostname, "example.org"); |
||||
assert_eq(url.port, ""); |
||||
assert_eq(url.pathname, "/path/to/resource"); |
||||
assert_eq(url.search, "?query"); |
||||
assert_eq(url.hash, "#fragment"); |
||||
"##, |
||||
), |
||||
]); |
||||
} |
||||
|
||||
#[test] |
||||
fn url_setters() { |
||||
// These were double checked against Firefox.
|
||||
run_test_actions([ |
||||
TestAction::run(TEST_HARNESS), |
||||
TestAction::run( |
||||
r##" |
||||
url = new URL("https://example.com:8080/path/to/resource?query#fragment"); |
||||
url.protocol = "http:"; |
||||
url.host = "example.org:80"; // Since protocol is http, port is removed.
|
||||
url.pathname = "/new/path"; |
||||
url.search = "?new-query"; |
||||
url.hash = "#new-fragment"; |
||||
assert_eq(url.href, "http://example.org/new/path?new-query#new-fragment"); |
||||
assert_eq(url.protocol, "http:"); |
||||
assert_eq(url.host, "example.org"); |
||||
assert_eq(url.hostname, "example.org"); |
||||
assert_eq(url.port, ""); |
||||
assert_eq(url.pathname, "/new/path"); |
||||
assert_eq(url.search, "?new-query"); |
||||
assert_eq(url.hash, "#new-fragment"); |
||||
"##, |
||||
), |
||||
]); |
||||
} |
||||
|
||||
#[test] |
||||
fn url_static_methods() { |
||||
run_test_actions([ |
||||
TestAction::run(TEST_HARNESS), |
||||
TestAction::run( |
||||
r##" |
||||
assert(URL.canParse("http://example.org/new/path?new-query#new-fragment")); |
||||
assert(!URL.canParse("http//:example.org/new/path?new-query#new-fragment")); |
||||
assert(!URL.canParse("http://example.org/new/path?new-query#new-fragment", "http:")); |
||||
assert(URL.canParse("/new/path?new-query#new-fragment", "http://example.org/")); |
||||
"##, |
||||
), |
||||
]); |
||||
} |
Loading…
Reference in new issue