mirror of
https://github.com/csd4ni3l/browser.git
synced 2026-04-17 16:06:03 +02:00
Compare commits
2 Commits
82ecd8cf8f
...
56f613f531
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56f613f531 | ||
|
|
6c0b5180b4 |
12
.github/workflows/main.yml
vendored
12
.github/workflows/main.yml
vendored
@@ -46,12 +46,12 @@ jobs:
|
|||||||
|
|
||||||
- name: Verify executable (Linux)
|
- name: Verify executable (Linux)
|
||||||
if: matrix.os == 'ubuntu-22.04'
|
if: matrix.os == 'ubuntu-22.04'
|
||||||
run: test target/release/soundboard
|
run: test target/release/csd4ni3l-browser
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Verify executable (Windows)
|
- name: Verify executable (Windows)
|
||||||
if: matrix.os == 'windows-latest'
|
if: matrix.os == 'windows-latest'
|
||||||
run: Test-Path target\release\soundboard.exe
|
run: Test-Path target\release\csd4ni3l-browser.exe
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
|
|
||||||
- name: Upload build artifact
|
- name: Upload build artifact
|
||||||
@@ -59,8 +59,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: ${{ matrix.platform }}
|
name: ${{ matrix.platform }}
|
||||||
path: |
|
path: |
|
||||||
target/release/soundboard
|
target/release/csd4ni3l-browser
|
||||||
target/release/soundboard.exe
|
target/release/csd4ni3l-browser.exe
|
||||||
|
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -91,5 +91,5 @@ jobs:
|
|||||||
--notes "Automated build for $TAG"
|
--notes "Automated build for $TAG"
|
||||||
fi
|
fi
|
||||||
# Upload the executables directly (no zip files)
|
# Upload the executables directly (no zip files)
|
||||||
gh release upload "$TAG" downloads/linux/soundboard --clobber
|
gh release upload "$TAG" downloads/linux/csd4ni3l-browser --clobber
|
||||||
gh release upload "$TAG" downloads/windows/soundboard.exe --clobber
|
gh release upload "$TAG" downloads/windows/csd4ni3l-browser.exe --clobber
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,4 @@
|
|||||||
python
|
python
|
||||||
/target
|
/target
|
||||||
|
css_cache
|
||||||
|
html_cache
|
||||||
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1843,6 +1843,7 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
|||||||
name = "csd4ni3l-browser"
|
name = "csd4ni3l-browser"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"base64",
|
||||||
"bevy",
|
"bevy",
|
||||||
"bevy_egui",
|
"bevy_egui",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
base64 = "0.22.1"
|
||||||
bevy_egui = "0.38.1"
|
bevy_egui = "0.38.1"
|
||||||
native-tls = "0.2.18"
|
native-tls = "0.2.18"
|
||||||
rand = "0.9.2"
|
rand = "0.9.2"
|
||||||
|
|||||||
@@ -1,12 +1,23 @@
|
|||||||
use std::{collections::HashMap, net::TcpStream, io::{Read, Write}, fs};
|
use std::{collections::HashMap, net::{Shutdown, TcpStream}, io::{Read, Write}, fs};
|
||||||
use native_tls::{TlsConnector, TlsStream};
|
use native_tls::{TlsConnector, TlsStream};
|
||||||
use crate::http_client::html_parser::{Node, Rule};
|
use base64::{engine::general_purpose::URL_SAFE, Engine as _};
|
||||||
|
use crate::http_client::html_parser::{CSSCache, CSSParser, CSSSelector, HTML, Node, get_inline_styles, tree_to_vec};
|
||||||
|
use serde_json;
|
||||||
|
|
||||||
enum Connection {
|
pub enum Connection {
|
||||||
Plain(TcpStream),
|
Plain(TcpStream),
|
||||||
Tls(TlsStream<TcpStream>),
|
Tls(TlsStream<TcpStream>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Connection {
|
||||||
|
fn shutdown(&mut self, how: Shutdown) -> std::io::Result<()> {
|
||||||
|
match self {
|
||||||
|
Connection::Plain(s) => s.shutdown(how),
|
||||||
|
Connection::Tls(s) => {let _ = s.shutdown(); Ok(())},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Read for Connection {
|
impl Read for Connection {
|
||||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||||
match self {
|
match self {
|
||||||
@@ -32,24 +43,25 @@ impl Write for Connection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resolve_url(scheme: &str, host: &str, port: u16, path: &str, mut url: &str) -> String {
|
pub fn resolve_url(scheme: &str, host: &str, port: u16, path: &str, url: &str) -> String {
|
||||||
if url.contains("://") {
|
let mut new_url = url;
|
||||||
return url.to_string();
|
if new_url.contains("://") {
|
||||||
|
return new_url.to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
let resolved_path = if !url.starts_with("/") {
|
let resolved_path = if !new_url.starts_with("/") {
|
||||||
let mut dir = path.rsplitn(2, '/').nth(1).unwrap_or("");
|
let mut dir = path.rsplitn(2, '/').nth(1).unwrap_or("");
|
||||||
|
|
||||||
while url.starts_with("../") {
|
while new_url.starts_with("../") {
|
||||||
url = url.strip_prefix("../").unwrap();
|
new_url = new_url.strip_prefix("../").unwrap();
|
||||||
if dir.contains('/') {
|
if dir.contains('/') {
|
||||||
dir = dir.rsplitn(2, '/').nth(1).unwrap_or("");
|
dir = dir.rsplitn(2, '/').nth(1).unwrap_or("");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
format!("{}/{}", dir, url)
|
format!("{}/{}", dir, new_url)
|
||||||
} else {
|
} else {
|
||||||
url.to_string()
|
new_url.to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
if resolved_path.starts_with("//") {
|
if resolved_path.starts_with("//") {
|
||||||
@@ -63,14 +75,14 @@ pub struct HTTPClient {
|
|||||||
pub scheme: String,
|
pub scheme: String,
|
||||||
pub host: String,
|
pub host: String,
|
||||||
pub path: String,
|
pub path: String,
|
||||||
pub port: u32,
|
pub port: u16,
|
||||||
pub request_headers: HashMap<String, String>,
|
pub request_headers: HashMap<String, String>,
|
||||||
pub response_explanation: Option<String>,
|
pub response_explanation: Option<String>,
|
||||||
pub response_headers: HashMap<String, String>,
|
pub response_headers: HashMap<String, String>,
|
||||||
pub response_http_version: Option<String>,
|
pub response_http_version: Option<String>,
|
||||||
pub response_status: Option<u32>,
|
pub response_status: Option<u32>,
|
||||||
pub nodes: Option<Vec<Node>>,
|
pub node: Option<Node>,
|
||||||
pub css_rules: Vec<Rule>,
|
pub css_rules: Vec<(CSSSelector, HashMap<String, String>)>,
|
||||||
pub content_response: String,
|
pub content_response: String,
|
||||||
pub view_source: bool,
|
pub view_source: bool,
|
||||||
pub redirect_count: u32,
|
pub redirect_count: u32,
|
||||||
@@ -90,7 +102,7 @@ impl HTTPClient {
|
|||||||
response_headers: HashMap::new(),
|
response_headers: HashMap::new(),
|
||||||
response_http_version: None,
|
response_http_version: None,
|
||||||
response_status: None,
|
response_status: None,
|
||||||
nodes: None,
|
node: None,
|
||||||
css_rules: Vec::new(),
|
css_rules: Vec::new(),
|
||||||
content_response: String::new(),
|
content_response: String::new(),
|
||||||
view_source: false,
|
view_source: false,
|
||||||
@@ -123,6 +135,7 @@ impl HTTPClient {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
let (host_str, path_str) = parsed_url_parts.split_once("/").unwrap();
|
let (host_str, path_str) = parsed_url_parts.split_once("/").unwrap();
|
||||||
|
self.host = host_str.to_string();
|
||||||
self.path = format!("/{}", path_str.to_string());
|
self.path = format!("/{}", path_str.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,16 +163,19 @@ impl HTTPClient {
|
|||||||
self.content_response = "".to_string();
|
self.content_response = "".to_string();
|
||||||
self.tcp_stream = None;
|
self.tcp_stream = None;
|
||||||
|
|
||||||
if !self.request_headers.contains_key("Host") {
|
if self.request_headers.contains_key("Host") {
|
||||||
self.request_headers.insert("Host".to_string(), self.host.clone());
|
self.request_headers.remove("Host");
|
||||||
}
|
}
|
||||||
|
|
||||||
let cache_filename = format!("{}_{}_{}_{}.html", self.scheme, self.host, self.port, self.path.replace("/", "_"));
|
self.request_headers.insert("Host".to_string(), self.host.clone());
|
||||||
if std::fs::exists(format!("html_cache/{}", cache_filename)).unwrap() {
|
|
||||||
|
let html_cache_key = URL_SAFE.encode(format!("{}_{}_{}_{}", self.scheme, self.host, self.port, self.path).as_bytes());
|
||||||
|
let html_cache_path = format!("html_cache/{}.html", html_cache_key);
|
||||||
|
if std::fs::exists(html_cache_path.clone()).unwrap() {
|
||||||
|
self.content_response = fs::read_to_string(html_cache_path).unwrap();
|
||||||
self.parse();
|
self.parse();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let tcp = TcpStream::connect(format!("{}:{}", self.host, self.port.to_string())).unwrap();
|
let tcp = TcpStream::connect(format!("{}:{}", self.host, self.port.to_string())).unwrap();
|
||||||
|
|
||||||
if self.scheme == "https" {
|
if self.scheme == "https" {
|
||||||
@@ -170,10 +186,100 @@ impl HTTPClient {
|
|||||||
self.tcp_stream = Some(Connection::Plain(tcp));
|
self.tcp_stream = Some(Connection::Plain(tcp));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let request_header_lines: String = self.request_headers
|
||||||
|
.iter()
|
||||||
|
.map(|(header_name, header_value)|{
|
||||||
|
format!("{}: {}", header_name, header_value)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\r\n");
|
||||||
|
|
||||||
|
let request = format!("GET {} HTTP/1.0\r\n{}\r\n\r\n", self.path, request_header_lines);
|
||||||
|
|
||||||
|
self.tcp_stream.as_mut().unwrap().write_all(request.as_bytes()).unwrap();
|
||||||
|
|
||||||
|
self.receive_response(css); // TODO: use threading
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn receive_response(&mut self, css: bool) {
|
fn receive_response(&mut self, css: bool) {
|
||||||
|
let mut temp_buffer = [0; 16384];
|
||||||
|
let mut headers_parsed: bool = false;
|
||||||
|
let mut content_length: Option<usize> = None;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let bytes_read = self.tcp_stream.as_mut().unwrap().read(&mut temp_buffer).unwrap_or(0);
|
||||||
|
if bytes_read == 0 {
|
||||||
|
println!("Connection closed by peer.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !headers_parsed {
|
||||||
|
let header_end_index = temp_buffer[..bytes_read].windows(4).position(|window| {window == b"\r\n\r\n"});
|
||||||
|
if let Some(header_end_index) = header_end_index {
|
||||||
|
let header_data = std::str::from_utf8(&temp_buffer[..header_end_index]).unwrap_or("");
|
||||||
|
let body_data = &temp_buffer[header_end_index + 4..bytes_read]; // +4 for the \r\n\r\n
|
||||||
|
|
||||||
|
self._parse_headers(header_data.to_string());
|
||||||
|
headers_parsed = true;
|
||||||
|
|
||||||
|
let content_length_header = self.response_headers.get("content-length");
|
||||||
|
if let Some(content_length_header) = content_length_header {
|
||||||
|
content_length = Some(content_length_header.parse().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.content_response = std::str::from_utf8(&body_data).unwrap_or("").to_string(); // Assuming body is UTF-8
|
||||||
|
|
||||||
|
if !content_length.is_none() && body_data.len() >= content_length.unwrap() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if content_length.is_none() {}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
self.content_response.push_str(std::str::from_utf8(&temp_buffer[..bytes_read]).unwrap_or(""));
|
||||||
|
if !content_length.is_none() && self.content_response.len() >= content_length.unwrap() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(ref mut stream) = self.tcp_stream {
|
||||||
|
stream.shutdown(Shutdown::Both).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.tcp_stream = None;
|
||||||
|
|
||||||
|
if 300 <= self.response_status.unwrap() && self.response_status.unwrap() < 400 {
|
||||||
|
if self.redirect_count >= 4 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.redirect_count += 1;
|
||||||
|
|
||||||
|
let headers = self.request_headers.clone();
|
||||||
|
let location = self.response_headers.get("location")
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or("/".to_string());
|
||||||
|
if location.starts_with("http") || location.starts_with("https") {
|
||||||
|
self.get_request(&location, headers, false);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
self.get_request(&format!("{}://{}{}", self.scheme, self.host, location), headers, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
self.redirect_count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !css {
|
||||||
|
if !(300..400).contains(&self.response_status.unwrap_or(0)) {
|
||||||
|
self.parse();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _parse_headers(&mut self, header_data: String) {
|
fn _parse_headers(&mut self, header_data: String) {
|
||||||
@@ -190,6 +296,7 @@ impl HTTPClient {
|
|||||||
self.response_http_version = Some(parts.next().unwrap().to_string());
|
self.response_http_version = Some(parts.next().unwrap().to_string());
|
||||||
self.response_status = Some(parts.next().unwrap().parse().unwrap());
|
self.response_status = Some(parts.next().unwrap().parse().unwrap());
|
||||||
let explanation_parts: Vec<&str> = parts.collect();
|
let explanation_parts: Vec<&str> = parts.collect();
|
||||||
|
self.response_explanation = Some(explanation_parts.join(" "));
|
||||||
|
|
||||||
let mut headers = HashMap::new();
|
let mut headers = HashMap::new();
|
||||||
for i in 1..lines.len() {
|
for i in 1..lines.len() {
|
||||||
@@ -200,13 +307,76 @@ impl HTTPClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let (header_name, value) = line.split_once(":").unwrap();
|
let (header_name, value) = line.split_once(":").unwrap();
|
||||||
headers.insert(header_name.trim().to_string(), value.trim().to_string());
|
headers.insert(header_name.trim().to_lowercase().to_string(), value.trim().to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
self.response_headers = headers;
|
self.response_headers = headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse(&mut self) {
|
pub fn parse(&mut self) {
|
||||||
|
self.css_rules.clear();
|
||||||
|
|
||||||
|
let html_cache_key = URL_SAFE.encode(format!("{}_{}_{}_{}", self.scheme, self.host, self.port, self.path).as_bytes());
|
||||||
|
let html_cache_path = format!("html_cache/{}.html", html_cache_key);
|
||||||
|
|
||||||
|
if std::fs::exists(html_cache_path.clone()).unwrap() {
|
||||||
|
self.content_response = std::fs::read_to_string(html_cache_path).unwrap();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let _ = std::fs::write(html_cache_path, self.content_response.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let original_scheme = self.scheme.clone();
|
||||||
|
let original_host = self.host.clone();
|
||||||
|
let original_port = self.port;
|
||||||
|
let original_path = self.path.clone();
|
||||||
|
let original_response = self.content_response.clone();
|
||||||
|
|
||||||
|
self.node = Some(Node::Element(HTML::new(self.content_response.clone()).parse()));
|
||||||
|
|
||||||
|
let mut flattened_tree = vec![];
|
||||||
|
tree_to_vec(self.node.as_ref().unwrap(), &mut flattened_tree);
|
||||||
|
|
||||||
|
let css_links: Vec<String> = flattened_tree.iter()
|
||||||
|
.filter(|node| {
|
||||||
|
matches!(node, Node::Element(_)) && node.tag().unwrap() == "link".to_string() && node.attributes().unwrap().get("rel").unwrap() == &"stylesheet".to_string() && node.attributes().unwrap().get("href").is_some()
|
||||||
|
})
|
||||||
|
.map(|node: &&Node| {
|
||||||
|
node.attributes().unwrap()["href"].clone()
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
for css_link in css_links {
|
||||||
|
self.content_response.clear();
|
||||||
|
|
||||||
|
// we need to include the other variables so for example /styles.css wouldnt be cached for all websites
|
||||||
|
let css_cache_key = URL_SAFE.encode(format!("{}_{}_{}_{}", self.scheme, self.host, self.port, css_link).as_bytes());
|
||||||
|
let css_cache_path = format!("css_cache/{}.json", css_cache_key);
|
||||||
|
|
||||||
|
let rules: Vec<(CSSSelector, HashMap<String, String>)> = if std::path::Path::new(&css_cache_path).exists() {
|
||||||
|
let css_cache_content = std::fs::read_to_string(&css_cache_path).unwrap();
|
||||||
|
let json: CSSCache = serde_json::from_str(&css_cache_content).unwrap();
|
||||||
|
json.css_cache
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let resolved = resolve_url(self.scheme.as_str(), self.host.as_str(), self.port, self.path.as_str(), css_link.as_str());
|
||||||
|
let headers = self.request_headers.clone();
|
||||||
|
self.get_request(&resolved, headers, true);
|
||||||
|
let parsed_css = CSSParser::new(self.content_response.clone()).parse();
|
||||||
|
let json = CSSCache { css_cache: parsed_css };
|
||||||
|
let _ = std::fs::write(&css_cache_path, serde_json::to_string(&json).unwrap());
|
||||||
|
json.css_cache
|
||||||
|
};
|
||||||
|
|
||||||
|
self.css_rules.extend(rules);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.css_rules.extend(get_inline_styles(self.node.as_ref().unwrap()));
|
||||||
|
|
||||||
|
self.scheme = original_scheme;
|
||||||
|
self.host = original_host;
|
||||||
|
self.port = original_port;
|
||||||
|
self.path = original_path;
|
||||||
|
self.content_response = original_response;
|
||||||
|
self.needs_render = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::constants::*;
|
use crate::constants::*;
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -23,6 +24,13 @@ pub enum Node {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Node {
|
impl Node {
|
||||||
|
pub fn attributes(&self) -> Option<&HashMap<String, String>> {
|
||||||
|
match self {
|
||||||
|
Node::Element(e) => Some(&e.attributes),
|
||||||
|
Node::Text(_) => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn tag(&self) -> Option<&str> {
|
pub fn tag(&self) -> Option<&str> {
|
||||||
match self {
|
match self {
|
||||||
Node::Element(e) => Some(&e.tag),
|
Node::Element(e) => Some(&e.tag),
|
||||||
@@ -215,6 +223,7 @@ impl HTML {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct TagSelector {
|
pub struct TagSelector {
|
||||||
pub tag: String,
|
pub tag: String,
|
||||||
pub priority: i32
|
pub priority: i32
|
||||||
@@ -237,14 +246,15 @@ impl TagSelector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct DescendantSelector {
|
pub struct DescendantSelector {
|
||||||
pub ancestor: Box<Rule>,
|
pub ancestor: Box<CSSSelector>,
|
||||||
pub descendant: Box<Rule>,
|
pub descendant: Box<CSSSelector>,
|
||||||
pub priority: i32
|
pub priority: i32
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DescendantSelector {
|
impl DescendantSelector {
|
||||||
pub fn new(ancestor: Rule, descendant: Rule) -> DescendantSelector {
|
pub fn new(ancestor: CSSSelector, descendant: CSSSelector) -> DescendantSelector {
|
||||||
let new_priority = ancestor.priority() + descendant.priority();
|
let new_priority = ancestor.priority() + descendant.priority();
|
||||||
DescendantSelector {
|
DescendantSelector {
|
||||||
ancestor: Box::new(ancestor),
|
ancestor: Box::new(ancestor),
|
||||||
@@ -277,31 +287,33 @@ impl DescendantSelector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum Rule {
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "type")]
|
||||||
|
pub enum CSSSelector {
|
||||||
TagSelector(TagSelector),
|
TagSelector(TagSelector),
|
||||||
DescendantSelector(DescendantSelector),
|
DescendantSelector(DescendantSelector),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Rule {
|
impl CSSSelector {
|
||||||
pub fn priority(&self) -> i32 {
|
pub fn priority(&self) -> i32 {
|
||||||
match self {
|
match self {
|
||||||
Rule::TagSelector(s) => s.priority,
|
CSSSelector::TagSelector(s) => s.priority,
|
||||||
Rule::DescendantSelector(s) => s.priority,
|
CSSSelector::DescendantSelector(s) => s.priority,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn matches(&self, node: &Node) -> bool {
|
pub fn matches(&self, node: &Node) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Rule::TagSelector(s) => s.matches(node),
|
CSSSelector::TagSelector(s) => s.matches(node),
|
||||||
Rule::DescendantSelector(s) => s.matches(node),
|
CSSSelector::DescendantSelector(s) => s.matches(node),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cascade_priority(rule: (Rule, HashMap<String, String>)) -> i32 {
|
pub fn cascade_priority(rule: (CSSSelector, HashMap<String, String>)) -> i32 {
|
||||||
rule.0.priority()
|
rule.0.priority()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_inline_styles(node: &Node) -> Vec<(Rule, HashMap<String, String>)> {
|
pub fn get_inline_styles(node: &Node) -> Vec<(CSSSelector, HashMap<String, String>)> {
|
||||||
let mut all_rules = vec![];
|
let mut all_rules = vec![];
|
||||||
|
|
||||||
if let Node::Element(elem) = node {
|
if let Node::Element(elem) = node {
|
||||||
@@ -327,7 +339,7 @@ pub struct CSSParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CSSParser {
|
impl CSSParser {
|
||||||
fn new (s: String) -> CSSParser {
|
pub fn new (s: String) -> CSSParser {
|
||||||
CSSParser {
|
CSSParser {
|
||||||
chars: s.chars().collect(),
|
chars: s.chars().collect(),
|
||||||
len: s.chars().count(),
|
len: s.chars().count(),
|
||||||
@@ -423,22 +435,22 @@ impl CSSParser {
|
|||||||
pairs
|
pairs
|
||||||
}
|
}
|
||||||
|
|
||||||
fn selector(&mut self) -> Result<Rule, String> {
|
fn selector(&mut self) -> Result<CSSSelector, String> {
|
||||||
let mut out = Rule::TagSelector(TagSelector::new(self.word()?.to_lowercase()));
|
let mut out = CSSSelector::TagSelector(TagSelector::new(self.word()?.to_lowercase()));
|
||||||
|
|
||||||
self.whitespace();
|
self.whitespace();
|
||||||
|
|
||||||
while self.i < self.len && self.chars[self.i] != '{' {
|
while self.i < self.len && self.chars[self.i] != '{' {
|
||||||
let tag = self.word()?;
|
let tag = self.word()?;
|
||||||
let descendant = Rule::TagSelector(TagSelector::new(tag.to_lowercase()));
|
let descendant = CSSSelector::TagSelector(TagSelector::new(tag.to_lowercase()));
|
||||||
out = Rule::DescendantSelector(DescendantSelector::new(out, descendant));
|
out = CSSSelector::DescendantSelector(DescendantSelector::new(out, descendant));
|
||||||
self.whitespace();
|
self.whitespace();
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse(&mut self) -> Vec<(Rule, HashMap<String, String>)> {
|
pub fn parse(&mut self) -> Vec<(CSSSelector, HashMap<String, String>)> {
|
||||||
let mut rules = vec![];
|
let mut rules = vec![];
|
||||||
while self.i < self.len {
|
while self.i < self.len {
|
||||||
self.whitespace();
|
self.whitespace();
|
||||||
@@ -480,3 +492,16 @@ impl CSSParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn tree_to_vec<'a>(tree: &'a Node, vec: &mut Vec<&'a Node>) {
|
||||||
|
vec.push(tree);
|
||||||
|
|
||||||
|
for child in tree.children() {
|
||||||
|
tree_to_vec(child, vec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct CSSCache {
|
||||||
|
pub css_cache: Vec<(CSSSelector, HashMap<String, String>)>
|
||||||
|
}
|
||||||
@@ -32,6 +32,16 @@ impl Renderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(&mut self, http_client: &HTTPClient, ui: &mut Ui) {
|
fn update_content(&mut self, http_client: &mut HTTPClient) {
|
||||||
|
self.widgets.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(&mut self, http_client: &mut HTTPClient, ui: &mut Ui) {
|
||||||
|
if http_client.needs_render {
|
||||||
|
self.update_content(http_client);
|
||||||
|
http_client.needs_render = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.label(http_client.content_response.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
45
src/main.rs
45
src/main.rs
@@ -5,7 +5,7 @@ use bevy_egui::{EguiContextSettings, EguiContexts, EguiPrimaryContextPass, EguiS
|
|||||||
pub mod constants;
|
pub mod constants;
|
||||||
pub mod http_client;
|
pub mod http_client;
|
||||||
use crate::constants::DEFAULT_HEADERS;
|
use crate::constants::DEFAULT_HEADERS;
|
||||||
use crate::http_client::{connection::HTTPClient, renderer::Renderer};
|
use crate::http_client::{html_parser::{tree_to_vec, Node}, connection::HTTPClient, renderer::Renderer};
|
||||||
|
|
||||||
struct Tab {
|
struct Tab {
|
||||||
url: String,
|
url: String,
|
||||||
@@ -41,6 +41,31 @@ impl Tab {
|
|||||||
} else {
|
} else {
|
||||||
self.http_client.get_request(&format!("https://{}", self.url), DEFAULT_HEADERS.clone(), false);
|
self.http_client.get_request(&format!("https://{}", self.url), DEFAULT_HEADERS.clone(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.update_title();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_title(&mut self){
|
||||||
|
let mut flattened_tree = vec![];
|
||||||
|
tree_to_vec(&self.http_client.node.as_ref().unwrap(), &mut flattened_tree);
|
||||||
|
|
||||||
|
self.title = flattened_tree.iter()
|
||||||
|
.find(|node| {
|
||||||
|
if let Node::Text(text_node) = node {
|
||||||
|
if let Some(parent) = &text_node.parent {
|
||||||
|
return parent.tag().map(|t| t == "title").unwrap_or(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
})
|
||||||
|
.and_then(|node| {
|
||||||
|
if let Node::Text(text_node) = node {
|
||||||
|
Some(text_node.text.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| self.url.clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,10 +151,17 @@ fn draw(mut contexts: EguiContexts, mut app_state: ResMut<AppState>) -> Result {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(set_active_tab_to) = set_active_tab_to {
|
if let Some(set_active_tab_to) = set_active_tab_to {
|
||||||
|
// set previous's url
|
||||||
|
let active_tab = app_state.active_tab.clone();
|
||||||
|
app_state.tabs[active_tab].url = app_state.current_url.clone();
|
||||||
|
|
||||||
app_state.active_tab = set_active_tab_to;
|
app_state.active_tab = set_active_tab_to;
|
||||||
|
|
||||||
|
// set new's url back
|
||||||
|
app_state.current_url = app_state.tabs[set_active_tab_to].url.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ui.button("+" ).clicked() {
|
if ui.button("+").clicked() {
|
||||||
let new_tab = Tab::new("about:blank");
|
let new_tab = Tab::new("about:blank");
|
||||||
|
|
||||||
app_state.tabs.push(new_tab);
|
app_state.tabs.push(new_tab);
|
||||||
@@ -139,13 +171,18 @@ fn draw(mut contexts: EguiContexts, mut app_state: ResMut<AppState>) -> Result {
|
|||||||
let available_width = ui.available_width();
|
let available_width = ui.available_width();
|
||||||
let available_height = ui.available_height();
|
let available_height = ui.available_height();
|
||||||
|
|
||||||
ui.add_sized([available_width, available_height / 20.0], egui::TextEdit::singleline(&mut app_state.current_url)).request_focus();
|
let response = ui.add_sized([available_width, available_height / 20.0], egui::TextEdit::singleline(&mut app_state.current_url));
|
||||||
|
if ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
||||||
|
let current_url = app_state.current_url.clone();
|
||||||
|
let active_tab = app_state.active_tab;
|
||||||
|
app_state.tabs[active_tab].request(current_url);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
let active_tab_index = app_state.active_tab.clone();
|
let active_tab_index = app_state.active_tab.clone();
|
||||||
let tab = &mut app_state.tabs[active_tab_index];
|
let tab = &mut app_state.tabs[active_tab_index];
|
||||||
tab.renderer.render(&tab.http_client, ui);
|
tab.renderer.render(&mut tab.http_client, ui);
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
Reference in New Issue
Block a user