Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a URL class to boa_runtime #4004

Merged
merged 4 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading