1use serde::{Deserialize, Serialize};
11
12pub type Result<T> = std::result::Result<T, BrowserError>;
14
15#[derive(Debug, thiserror::Error)]
17pub enum BrowserError {
18 #[error("HTTP error: {0}")]
19 Http(String),
20
21 #[error("Parse error: {0}")]
22 Parse(String),
23
24 #[error("Invalid URL: {0}")]
25 InvalidUrl(String),
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct Page {
31 pub url: String,
33
34 pub html: String,
36
37 pub title: Option<String>,
39
40 pub status: u16,
42}
43
44impl Page {
45 pub fn new(url: String, html: String, status: u16) -> Self {
47 let title = extract_title(&html);
48 Self {
49 url,
50 html,
51 title,
52 status,
53 }
54 }
55
56 pub fn text(&self) -> String {
58 strip_html_tags(&self.html)
60 }
61
62 pub fn len(&self) -> usize {
64 self.html.len()
65 }
66
67 pub fn is_empty(&self) -> bool {
69 self.html.is_empty()
70 }
71}
72
73pub struct WebClient {
75 client: reqwest::Client,
77}
78
79impl WebClient {
80 pub fn new() -> Self {
82 Self {
83 client: reqwest::Client::new(),
84 }
85 }
86
87 pub async fn fetch(&self, url: &str) -> Result<Page> {
89 let response = self
90 .client
91 .get(url)
92 .send()
93 .await
94 .map_err(|e| BrowserError::Http(e.to_string()))?;
95
96 let status = response.status().as_u16();
97 let html = response
98 .text()
99 .await
100 .map_err(|e| BrowserError::Http(e.to_string()))?;
101
102 Ok(Page::new(url.to_string(), html, status))
103 }
104}
105
106impl Default for WebClient {
107 fn default() -> Self {
108 Self::new()
109 }
110}
111
112fn extract_title(html: &str) -> Option<String> {
114 let title_start = html.find("<title>")?;
116 let title_end = html[title_start..].find("</title>")?;
117 let title = &html[title_start + 7..title_start + title_end];
118 Some(title.trim().to_string())
119}
120
121fn strip_html_tags(html: &str) -> String {
123 let mut result = String::new();
125 let mut in_tag = false;
126
127 for ch in html.chars() {
128 match ch {
129 '<' => in_tag = true,
130 '>' => in_tag = false,
131 _ if !in_tag => result.push(ch),
132 _ => {}
133 }
134 }
135
136 result
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn test_page_creation() {
145 let page = Page::new(
146 "https://example.com".to_string(),
147 "<html><title>Test</title><body>Content</body></html>".to_string(),
148 200,
149 );
150
151 assert_eq!(page.url, "https://example.com");
152 assert_eq!(page.status, 200);
153 assert_eq!(page.title, Some("Test".to_string()));
154 }
155
156 #[test]
157 fn test_extract_title() {
158 let html = "<html><head><title>Test Title</title></head></html>";
159 assert_eq!(extract_title(html), Some("Test Title".to_string()));
160 }
161
162 #[test]
163 fn test_strip_html_tags() {
164 let html = "<html><body><p>Hello <b>World</b></p></body></html>";
165 let text = strip_html_tags(html);
166 assert!(text.contains("Hello"));
167 assert!(text.contains("World"));
168 assert!(!text.contains("<"));
169 }
170
171 #[test]
172 fn test_web_client_creation() {
173 let _client = WebClient::new();
174 assert!(true); }
176}