Skip to content

Commit

Permalink
Add a URL class to boa_runtime (#4004)
Browse files Browse the repository at this point in the history
* 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 comments
  • Loading branch information
hansl authored Oct 9, 2024
1 parent 67f4884 commit 94d08fe
Show file tree
Hide file tree
Showing 8 changed files with 426 additions and 10 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 3 additions & 8 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,12 @@ use boa_engine::{
builtins::promise::PromiseState,
context::ContextBuilder,
job::{FutureJob, JobQueue, NativeJob},
js_string,
module::{Module, SimpleModuleLoader},
optimizer::OptimizerOptions,
property::Attribute,
script::Script,
vm::flowgraph::{Direction, Graph},
Context, JsError, JsNativeError, JsResult, Source,
};
use boa_runtime::Console;
use clap::{Parser, ValueEnum, ValueHint};
use colored::Colorize;
use debug::init_boa_debug_object;
Expand Down Expand Up @@ -442,12 +439,10 @@ fn main() -> Result<(), io::Error> {
Ok(())
}

/// Adds the CLI runtime to the context.
/// Adds the CLI runtime to the context with default options.
fn add_runtime(context: &mut Context) {
let console = Console::init(context);
context
.register_global_property(js_string!(Console::NAME), console, Attribute::all())
.expect("the console object shouldn't exist");
boa_runtime::register(context, boa_runtime::RegisterOptions::new())
.expect("should not fail while registering the runtime");
}

#[derive(Default)]
Expand Down
5 changes: 5 additions & 0 deletions core/runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ boa_engine.workspace = true
boa_gc.workspace = true
boa_interop.workspace = true
rustc-hash = { workspace = true, features = ["std"] }
url = { version = "2.5.2", optional = true }

[dev-dependencies]
indoc.workspace = true
Expand All @@ -25,3 +26,7 @@ workspace = true

[package.metadata.docs.rs]
all-features = true

[features]
default = ["all"]
all = ["url"]
4 changes: 2 additions & 2 deletions core/runtime/src/console/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ pub trait Logger: Trace + Sized {
/// Implements the [`Logger`] trait and output errors to stderr and all
/// the others to stdout. Will add indentation based on the number of
/// groups.
#[derive(Trace, Finalize)]
struct DefaultLogger;
#[derive(Debug, Trace, Finalize)]
pub struct DefaultLogger;

impl Logger for DefaultLogger {
#[inline]
Expand Down
53 changes: 53 additions & 0 deletions core/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,60 @@ mod text;
#[doc(inline)]
pub use text::{TextDecoder, TextEncoder};

pub mod url;

/// Options used when registering all built-in objects and functions of the `WebAPI` runtime.
#[derive(Debug)]
pub struct RegisterOptions<L: Logger> {
console_logger: L,
}

impl Default for RegisterOptions<console::DefaultLogger> {
fn default() -> Self {
Self {
console_logger: console::DefaultLogger,
}
}
}

impl RegisterOptions<console::DefaultLogger> {
/// Create a new `RegisterOptions` with the default options.
#[must_use]
pub fn new() -> Self {
Self::default()
}
}

impl<L: Logger> RegisterOptions<L> {
/// Set the logger for the console object.
pub fn with_console_logger<L2: Logger>(self, logger: L2) -> RegisterOptions<L2> {
RegisterOptions::<L2> {
console_logger: logger,
}
}
}

/// Register all the built-in objects and functions of the `WebAPI` runtime.
///
/// # Errors
/// This will error is any of the built-in objects or functions cannot be registered.
pub fn register(
ctx: &mut boa_engine::Context,
options: RegisterOptions<impl Logger + 'static>,
) -> boa_engine::JsResult<()> {
Console::register_with_logger(ctx, options.console_logger)?;
TextDecoder::register(ctx)?;
TextEncoder::register(ctx)?;

#[cfg(feature = "url")]
url::Url::register(ctx)?;

Ok(())
}

#[cfg(test)]
pub(crate) mod test {
use crate::{register, RegisterOptions};
use boa_engine::{builtins, Context, JsResult, JsValue, Source};
use std::borrow::Cow;

Expand Down Expand Up @@ -126,6 +178,7 @@ pub(crate) mod test {
#[track_caller]
pub(crate) fn run_test_actions(actions: impl IntoIterator<Item = TestAction>) {
let context = &mut Context::default();
register(context, RegisterOptions::default()).expect("failed to register WebAPI objects");
run_test_actions_with(actions, context);
}

Expand Down
236 changes: 236 additions & 0 deletions core/runtime/src/url.rs
Original file line number Diff line number Diff line change
@@ -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))
}
}
}
Loading

0 comments on commit 94d08fe

Please sign in to comment.