diff --git a/crates/browser/src/loader.rs b/crates/browser/src/loader.rs index 3f492b9..f4c0097 100644 --- a/crates/browser/src/loader.rs +++ b/crates/browser/src/loader.rs @@ -101,13 +101,18 @@ impl ResourceLoader { /// text resources using the appropriate character encoding (per WHATWG spec), /// and returns the result as a typed `Resource`. /// - /// Handles `data:` URLs locally without network access. + /// Handles `data:` and `about:` URLs locally without network access. pub fn fetch(&mut self, url: &Url) -> Result { // Handle data: URLs without network fetch. if url.scheme() == "data" { return fetch_data_url(&url.serialize()); } + // Handle about: URLs without network fetch. + if url.scheme() == "about" { + return fetch_about_url(url); + } + let response = self.client.get(url)?; // Check for HTTP error status codes @@ -170,13 +175,20 @@ impl ResourceLoader { /// Fetch a URL string, resolving it against an optional base URL. /// - /// Handles `data:` URLs locally without network access. + /// Handles `data:` and `about:` URLs locally without network access. pub fn fetch_url(&mut self, url_str: &str, base: Option<&Url>) -> Result { // Handle data URLs directly — no network fetch needed. if is_data_url(url_str) { return fetch_data_url(url_str); } + // Handle about: URLs without network fetch. + if url_str.starts_with("about:") { + let url = + Url::parse(url_str).map_err(|_| LoadError::InvalidUrl(url_str.to_string()))?; + return fetch_about_url(&url); + } + let url = match base { Some(base_url) => Url::parse_with_base(url_str, base_url) .or_else(|_| Url::parse(url_str)) @@ -328,6 +340,30 @@ fn fetch_data_url(url_str: &str) -> Result { } } +// --------------------------------------------------------------------------- +// about: URL handling +// --------------------------------------------------------------------------- + +/// The minimal HTML document for about:blank. +pub const ABOUT_BLANK_HTML: &str = ""; + +/// Fetch an about: URL, returning the appropriate resource. +/// +/// Currently only `about:blank` is supported, which returns an empty HTML +/// document with UTF-8 encoding. +fn fetch_about_url(url: &Url) -> Result { + match url.path().as_str() { + "blank" => Ok(Resource::Html { + text: ABOUT_BLANK_HTML.to_string(), + base_url: url.clone(), + encoding: Encoding::Utf8, + }), + other => Err(LoadError::InvalidUrl(format!( + "unsupported about: URL: about:{other}" + ))), + } +} + /// Map a charset name to an Encoding, defaulting to UTF-8. fn charset_to_encoding(charset: Option<&str>) -> Encoding { charset @@ -715,4 +751,84 @@ mod tests { other => panic!("expected Other, got {:?}", other), } } + + // ----------------------------------------------------------------------- + // about: URL loading + // ----------------------------------------------------------------------- + + #[test] + fn about_blank_via_fetch_url() { + let mut loader = ResourceLoader::new(); + let result = loader.fetch_url("about:blank", None); + assert!(result.is_ok()); + match result.unwrap() { + Resource::Html { + text, + encoding, + base_url, + .. + } => { + assert_eq!(text, ABOUT_BLANK_HTML); + assert_eq!(encoding, Encoding::Utf8); + assert_eq!(base_url.scheme(), "about"); + } + other => panic!("expected Html, got {:?}", other), + } + } + + #[test] + fn about_blank_via_fetch() { + let mut loader = ResourceLoader::new(); + let url = Url::parse("about:blank").unwrap(); + let result = loader.fetch(&url); + assert!(result.is_ok()); + match result.unwrap() { + Resource::Html { + text, + encoding, + base_url, + .. + } => { + assert_eq!(text, ABOUT_BLANK_HTML); + assert_eq!(encoding, Encoding::Utf8); + assert_eq!(base_url.scheme(), "about"); + } + other => panic!("expected Html, got {:?}", other), + } + } + + #[test] + fn about_blank_dom_structure() { + let doc = we_html::parse_html(ABOUT_BLANK_HTML); + + // Find the element under the document root. + let html = doc + .children(doc.root()) + .find(|&n| doc.tag_name(n) == Some("html")); + assert!(html.is_some(), "document should have an element"); + let html = html.unwrap(); + + // The DOM should have html > head + body structure. + let children: Vec<_> = doc + .children(html) + .filter(|&n| doc.tag_name(n).is_some()) + .collect(); + assert_eq!(children.len(), 2); + assert_eq!(doc.tag_name(children[0]).unwrap(), "head"); + assert_eq!(doc.tag_name(children[1]).unwrap(), "body"); + + // Body should have no child elements. + let body_children: Vec<_> = doc + .children(children[1]) + .filter(|&n| doc.tag_name(n).is_some()) + .collect(); + assert!(body_children.is_empty()); + } + + #[test] + fn about_unsupported_url() { + let mut loader = ResourceLoader::new(); + let result = loader.fetch_url("about:invalid", None); + assert!(matches!(result, Err(LoadError::InvalidUrl(_)))); + } } diff --git a/crates/browser/src/main.rs b/crates/browser/src/main.rs index 1162f2c..d821388 100644 --- a/crates/browser/src/main.rs +++ b/crates/browser/src/main.rs @@ -1,6 +1,7 @@ use std::cell::RefCell; use std::collections::HashMap; +use we_browser::loader::{ResourceLoader, ABOUT_BLANK_HTML}; use we_html::parse_html; use we_layout::layout; use we_platform::appkit; @@ -9,26 +10,6 @@ use we_render::Renderer; use we_style::computed::{extract_stylesheets, resolve_styles}; use we_text::font::{self, Font}; -/// Default HTML page shown when no file argument is provided. -const DEFAULT_HTML: &str = r#" - - -we browser - - - -

Hello from we!

-

This is a from-scratch web browser engine written in pure Rust.

-

Zero external crate dependencies. Every subsystem is implemented in Rust.

-

Features

-

HTML5 tokenizer, DOM tree, block layout, CSS cascade, and software rendering.

- -"#; - /// Browser state kept in thread-local storage so the resize handler can /// access it. All AppKit callbacks run on the main thread. struct BrowserState { @@ -109,17 +90,46 @@ fn handle_resize(width: f64, height: f64) { }); } -fn main() { - // Load HTML from file argument or use default page. - let html = match std::env::args().nth(1) { - Some(path) => match std::fs::read_to_string(&path) { - Ok(content) => content, +/// Load content from a command-line argument. +/// +/// Tries the argument as a URL first (http://, https://, about:, data:), +/// then falls back to reading it as a file path. +fn load_from_arg(arg: &str) -> String { + // Try as URL if it has a recognized scheme. + if arg.starts_with("http://") + || arg.starts_with("https://") + || arg.starts_with("about:") + || arg.starts_with("data:") + { + let mut loader = ResourceLoader::new(); + match loader.fetch_url(arg, None) { + Ok(we_browser::loader::Resource::Html { text, .. }) => return text, + Ok(_) => { + eprintln!("URL did not return HTML: {arg}"); + std::process::exit(1); + } Err(e) => { - eprintln!("Error reading {}: {}", path, e); + eprintln!("Error loading {arg}: {e}"); std::process::exit(1); } - }, - None => DEFAULT_HTML.to_string(), + } + } + + // Fall back to file path. + match std::fs::read_to_string(arg) { + Ok(content) => content, + Err(e) => { + eprintln!("Error reading {arg}: {e}"); + std::process::exit(1); + } + } +} + +fn main() { + // Load HTML from argument (URL, file path) or default to about:blank. + let html = match std::env::args().nth(1) { + Some(arg) => load_from_arg(&arg), + None => ABOUT_BLANK_HTML.to_string(), }; // Load a system font for text rendering.