diff --git a/Cargo.lock b/Cargo.lock index 07e9c16..f0a891d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -560,7 +560,7 @@ dependencies = [ [[package]] name = "koxinga" -version = "0.1.1" +version = "0.2.0-beta.0" dependencies = [ "ming-wm-lib", "reqwest", @@ -604,9 +604,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "ming-wm-lib" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc80b6035509629ecba931bc6851513fca9fa8bef2be965b8bd437dd00b3930" +checksum = "729186cd7726de48643a22cd4af3e66d5198c8a6a9e08f214aa4602ce625b2ad" [[package]] name = "miniz_oxide" diff --git a/Cargo.toml b/Cargo.toml index 2ea0ebc..289ca2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,10 +3,16 @@ name = "koxinga" version = "0.2.0-beta.0" edition = "2021" +[lints.clippy] +len_zero = "allow" +comparison_to_empty = "allow" +redundant_static_lifetimes = "allow" +vec_box = "allow" + +[dependencies] +ming-wm-lib = "0.2.3" +reqwest = { version = "0.12", features = [ "blocking" ] } + [[bin]] name = "mingInternet_Koxinga_Browser" path = "src/main.rs" - -[dependencies] -ming-wm-lib = "0.2.2" -reqwest = { version = "0.12", features = [ "blocking" ] } diff --git a/koxinga.md b/koxinga.md index 85534a9..64ce257 100644 --- a/koxinga.md +++ b/koxinga.md @@ -4,7 +4,10 @@ Koxinga is a web browser supporting text and links. - `u`: URL mode, where a URL can be inputted. Hit enter/return to go to that page. - `l`: Link mode. The page will now show numbers in front of any links. Input the number corresponding to the link to navigate to, then hit enter/return. -- `s`: Search mode. Search engine is https://old-search.marginalia.nu +- `i`: Input mode. Fill in text inputs using the format "0,inputname=input value". +- `f`: Submit Form mode. Enter in form number to submit. +- `s`: Search mode. Search for text on the page - `j`, `k` to scroll page. -- `0`: Go to top of page. +- `j`, `k` to move down/up lines. +- `gg`: Go to top of page. - `G`: Go to bottom of page. diff --git a/koxinga_hn.png b/koxinga_hn.png index e6d2e62..09db0da 100644 Binary files a/koxinga_hn.png and b/koxinga_hn.png differ diff --git a/koxinga_wiki.png b/koxinga_wiki.png index daa868e..022be2d 100644 Binary files a/koxinga_wiki.png and b/koxinga_wiki.png differ diff --git a/koxinga_within.png b/koxinga_within.png index f7ef84e..a603bea 100644 Binary files a/koxinga_within.png and b/koxinga_within.png differ diff --git a/src/http.rs b/src/http.rs index 99f89cc..250dd53 100644 --- a/src/http.rs +++ b/src/http.rs @@ -24,4 +24,6 @@ impl HttpClient { } None } + + //todo: form submit (get/post) } diff --git a/src/main.rs b/src/main.rs index e5d5027..75bc95b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,36 +2,32 @@ use std::vec::Vec; use std::vec; use std::fmt; use std::boxed::Box; +use std::collections::HashMap; //use ming_wm_lib::logging::log; use ming_wm_lib::window_manager_types::{ DrawInstructions, WindowLike, WindowLikeType }; use ming_wm_lib::messages::{ WindowMessage, WindowMessageResponse }; -use ming_wm_lib::utils::Substring; -use ming_wm_lib::framebuffer_types::Dimensions; +use ming_wm_lib::utils::{ get_rest_of_split, Substring }; +use ming_wm_lib::framebuffer_types::{ Dimensions, RGBColor }; use ming_wm_lib::themes::ThemeInfo; -use ming_wm_lib::fonts::measure_text; +use ming_wm_lib::fonts::{ CachedFontCharGetter, measure_text, measure_text_with_cache }; use ming_wm_lib::ipc::listen; mod http; use crate::http::HttpClient; mod xml; -use crate::xml::{ parse, Node, OutputType }; +use crate::xml::{ parse, remove_quotes, Form, FormSubmitMethod, Node, OutputType }; +mod url; +use crate::url::Url; const LINE_HEIGHT: usize = 18; +const BAND_HEIGHT: usize = 19; -fn get_base_url(url: &str) -> String { - let mut base_url = String::new(); - let mut slash = 0; - for c in url.chars() { - if c == '/' { - slash += 1; - if slash == 3 { - break; - } - } - base_url += &c.to_string(); - } - base_url +#[derive(Default, PartialEq)] +enum State { + #[default] + None, + Maybeg, } #[derive(Default, PartialEq)] @@ -41,6 +37,8 @@ enum Mode { Url, Link, Search, + FormSubmit, + FormInput, //(input elements) } impl fmt::Display for Mode { @@ -50,24 +48,57 @@ impl fmt::Display for Mode { Mode::Url => "URL", Mode::Link => "LINK", Mode::Search => "SEARCH", + Mode::FormSubmit => "FORM SUBMIT", + Mode::FormInput => "FORM INPUT", })?; Ok(()) } } +#[derive(Clone, Copy, PartialEq)] +pub enum Subtype { + Text, + Link, + TextInput, + Button, + // +} + +impl Subtype { + pub fn to_rgb(&self, theme_info: &ThemeInfo) -> RGBColor { + match self { + Self::Text => theme_info.text, + Self::Link => theme_info.alt_text, + Self::TextInput => theme_info.alt_secondary, + Self::Button => theme_info.alt_secondary, + } + } + + // + pub fn is_one_off(&self) -> bool { + //button, text input, stuff that we don't expect other subtypes to be in (well, buttons might, but whatever) + self == &Subtype::TextInput || self == &Subtype::Button + } +} + #[derive(Default)] struct KoxingaBrowser { client: HttpClient, dimensions: Dimensions, + fonts: Vec, mode: Mode, + state: State, max_lines: usize, top_line_no: usize, - url: Option, + url: Option, input: String, + maybe_num: Option, links: Vec, + forms: Vec
, + form_inputs: HashMap<(usize, String), String>, //form #+input name, input value title: Option, top_level_nodes: Vec>, - page: Vec<(usize, usize, String, bool)>, //x, y, text, link colour or not + page: Vec<(usize, usize, String, Subtype)>, //x, y, text, subtype } impl WindowLike for KoxingaBrowser { @@ -79,93 +110,198 @@ impl WindowLike for KoxingaBrowser { }, WindowMessage::ChangeDimensions(dimensions) => { self.dimensions = dimensions; - self.calc_page(); + self.calc_page(false); WindowMessageResponse::JustRedraw }, WindowMessage::KeyPress(key_press) => { match self.mode { Mode::Normal => { - let max_lines_screen = (self.dimensions[1] - 2) / LINE_HEIGHT - 1; + let max_lines_screen = (self.dimensions[1] - 2) / LINE_HEIGHT - 2; + if self.state == State::Maybeg && key_press.key != 'g' { + self.state = State::None; + } if key_press.key == 'u' { self.mode = Mode::Url; - self.input = self.url.clone().unwrap_or(String::new()); + self.input = self.url.clone().unwrap_or(Url::new(String::new())).to_string(); WindowMessageResponse::JustRedraw } else if key_press.key == 'l' && self.url.is_some() { self.mode = Mode::Link; - self.calc_page(); + self.calc_page(false); + WindowMessageResponse::JustRedraw + } else if key_press.key == 'f' { + self.mode = Mode::FormSubmit; + self.calc_page(false); WindowMessageResponse::JustRedraw } else if key_press.key == 's' { self.mode = Mode::Search; WindowMessageResponse::JustRedraw - } else if key_press.key == 'k' { - if self.top_line_no > 0 { - self.top_line_no -= 1; - } + } else if key_press.key == 'f' && self.url.is_some() { + self.mode = Mode::FormSubmit; + self.calc_page(false); WindowMessageResponse::JustRedraw - } else if key_press.key == 'j' { - if self.top_line_no < self.max_lines - max_lines_screen + 1 { - self.top_line_no += 1; + } else if key_press.key == 'i' && self.url.is_some() { + self.mode = Mode::FormInput; + self.calc_page(false); + WindowMessageResponse::JustRedraw + } else if key_press.key == 'j' || key_press.key == 'k' { + let num = self.maybe_num.unwrap_or(1); + self.maybe_num = None; + if key_press.key == 'j' { + let max_top = self.max_lines - max_lines_screen + 1; + if self.top_line_no + num < max_top { + self.top_line_no += num; + WindowMessageResponse::JustRedraw + } else if self.top_line_no != max_top { + self.top_line_no = max_top; + WindowMessageResponse::JustRedraw + } else { + WindowMessageResponse::DoNothing + } + } else { + if self.top_line_no > num { + self.top_line_no -= num; + WindowMessageResponse::JustRedraw + } else if self.top_line_no > 0 { + self.top_line_no = 0; + WindowMessageResponse::JustRedraw + } else { + WindowMessageResponse::DoNothing + } + } + } else if key_press.key == 'g' { + if self.state == State::Maybeg { + self.top_line_no = 0; WindowMessageResponse::JustRedraw } else { + self.state = State::Maybeg; WindowMessageResponse::DoNothing } - } else if key_press.key == '0' { - self.top_line_no = 0; - WindowMessageResponse::JustRedraw } else if key_press.key == 'G' { self.top_line_no = self.max_lines - max_lines_screen + 1; WindowMessageResponse::JustRedraw + } else if key_press.key.is_ascii_digit() { + self.maybe_num = Some(self.maybe_num.unwrap_or(0) * 10 + key_press.key.to_digit(10).unwrap() as usize); + WindowMessageResponse::DoNothing + } else if self.maybe_num.is_some() { + self.maybe_num = None; + WindowMessageResponse::DoNothing } else { WindowMessageResponse::DoNothing } }, - Mode::Url | Mode::Search | Mode::Link => { + //all modes besides normal, which use the bottom input + _ => { if key_press.is_enter() && self.input.len() > 0 { - let new_url = if self.mode == Mode::Search { - "https://old-search.marginalia.nu/search?query=".to_string() + &self.input - } else if self.mode == Mode::Link { - self.mode = Mode::Normal; - let link_index = self.input.parse::().unwrap(); - let url = self.url.as_ref().unwrap(); - let mut link; - if link_index < self.links.len() { - link = self.links[link_index].clone(); - if link.chars().count() >= 2 { - //remove the quotes - link = link.substring(1, link.len() - 1).to_string(); - } - if link.starts_with("/") { - link = get_base_url(&url) + &link; - } else if !link.starts_with("http://") && !link.starts_with("https://") { - link = url.clone() + if url.ends_with("/") { "" } else { "/" } + &link; + if self.mode == Mode::Url || self.mode == Mode::Link { + let new_url = if self.mode == Mode::Link { + self.mode = Mode::Normal; + let link_index = self.input.parse::().unwrap(); + let mut url = self.url.as_ref().unwrap().clone(); + if link_index < self.links.len() { + let mut link = self.links[link_index].clone(); + if link.chars().count() >= 2 { + link = remove_quotes(link); + } + if link.starts_with("/") { + url.pop_to_root(); + url.append(link); + } else if !link.starts_with("http://") && !link.starts_with("https://") { + if !link.starts_with("?") && !link.starts_with("#") { + url.pop(); + } + url.append(link); + } else { + url = Url::new(link); + } + } else { + return WindowMessageResponse::DoNothing } + url } else { - return WindowMessageResponse::DoNothing + //if Mode::Url + Url::new(self.input.clone()) + }; + if let Some(text) = self.client.get(&new_url.to_string()) { + self.change_url(new_url, text); + WindowMessageResponse::JustRedraw + } else { + WindowMessageResponse::DoNothing + } + } else if self.mode == Mode::FormSubmit || self.mode == Mode::FormInput { + if self.mode == Mode::FormInput { + //this shouldn't be able to panic I hope + //get_rest_of_split may return an empty string, but it won't panic + let mut splitted = self.input.split("="); + let first = splitted.next().unwrap(); + let input_value = get_rest_of_split(&mut splitted, Some("=")); + let mut first_splitted = first.split(","); + let form_count = first_splitted.next().unwrap().parse::(); + //form count is not a number + if form_count.is_err() { + self.input = String::new(); + return WindowMessageResponse::JustRedraw; + } + let form_count = form_count.unwrap(); + let input_name = get_rest_of_split(&mut first_splitted, None); //I mean, there shouldn't be a comma in the input name, right? + //insert overwrites + //todo: check if exists first + self.form_inputs.insert((form_count, input_name), input_value); + self.input = String::new(); + self.calc_page(false); + WindowMessageResponse::JustRedraw + } else { + //form submit + let form_index = self.input.parse::().unwrap(); + if form_index < self.forms.len() { + let form_info = &self.forms[form_index]; + match form_info.method { + FormSubmitMethod::Get => { + //construct url to redirect to + let mut form_url = Url::new_maybe_relative(form_info.action.clone(), self.url.clone().unwrap()); + //key aka name attr + for key in &form_info.input_names { + if let Some(value) = self.form_inputs.get(&(form_index, key.clone())) { + form_url.append_query(&key, value); + } + } + //log(&format!("{}", form_url.clone())); + if let Some(text) = self.client.get(&form_url.to_string()) { + self.change_url(form_url, text); + WindowMessageResponse::JustRedraw + } else { + WindowMessageResponse::DoNothing + } + }, + FormSubmitMethod::Post => { + //todo. maybe later + // + WindowMessageResponse::DoNothing + }, + } + } else { + WindowMessageResponse::DoNothing + } } - link - } else { - //if Mode::Url - self.input.clone() - }; - if let Some(text) = self.client.get(&new_url) { - self.url = Some(new_url.clone()); - self.top_line_no = 0; - //log(&text); - self.top_level_nodes = parse(&text); - self.input = String::new(); - self.calc_page(); - self.mode = Mode::Normal; - WindowMessageResponse::JustRedraw } else { + //Mode::Search + for p in &self.page { + let line_no = (p.1 - 2) / LINE_HEIGHT; + if line_no > self.top_line_no { + //p.2 is the text + if p.2.contains(&self.input) { + self.top_line_no = line_no; + return WindowMessageResponse::JustRedraw; + } + } + } WindowMessageResponse::DoNothing } } else if key_press.is_escape() { - let is_link_mode = self.mode == Mode::Link; self.mode = Mode::Normal; - if is_link_mode { - self.calc_page(); - } self.input = String::new(); + if self.mode == Mode::Link || self.mode == Mode::FormSubmit || self.mode == Mode::FormInput { + self.calc_page(false); + } WindowMessageResponse::JustRedraw } else if key_press.is_backspace() && self.input.len() > 0 { self.input = self.input.remove_last(); @@ -185,26 +321,39 @@ impl WindowLike for KoxingaBrowser { fn draw(&self, theme_info: &ThemeInfo) -> Vec { let mut instructions = Vec::new(); - let max_lines_screen = (self.dimensions[1] - 2) / LINE_HEIGHT - 1; + let max_lines_screen = (self.dimensions[1] - 2) / LINE_HEIGHT - 2; for p in &self.page { let line_no = (p.1 - 2) / LINE_HEIGHT; if line_no >= self.top_line_no + max_lines_screen { break; } else if line_no >= self.top_line_no && line_no < self.top_line_no + max_lines_screen { - instructions.push(DrawInstructions::Text([p.0, p.1 - LINE_HEIGHT * self.top_line_no], vec!["nimbus-roman".to_string()], p.2.clone(), if p.3 { theme_info.top_text } else { theme_info.text }, theme_info.background, Some(1), None)); + let subtype = p.3; + let top_left = [p.0, p.1 - LINE_HEIGHT * self.top_line_no]; + let bg_colour = if subtype == Subtype::TextInput || subtype == Subtype::Button { + Some(theme_info.alt_background) + } else { + None + }; + if let Some(bg_colour) = bg_colour { + let width = measure_text(&self.fonts, &p.2, Some(1)).width; + instructions.push(DrawInstructions::Rect([top_left[0] - 2, top_left[1] - 2], [width, LINE_HEIGHT], bg_colour)); + } + instructions.push(DrawInstructions::Text(top_left, self.fonts.clone(), p.2.clone(), subtype.to_rgb(&theme_info), bg_colour.unwrap_or(theme_info.background), Some(1), None)); } } - //mode + //mode, in a blue band + instructions.push(DrawInstructions::Rect([0, self.dimensions[1] - BAND_HEIGHT * 2], [self.dimensions[0], BAND_HEIGHT], theme_info.top)); let mut bottom_text = self.mode.to_string() + ": "; - if self.mode == Mode::Normal && self.dimensions[0] >= 500 { - bottom_text += "u (url), s (search)"; - if self.url.is_some() { - bottom_text += ", l (link), j (down), k (up)"; + if self.mode == Mode::Normal && self.dimensions[0] >= 300 { + bottom_text += "u(rl)"; + if self.url.is_some() && self.dimensions[0] >= 640 { + bottom_text += ", s(earch), l(ink), i(nput), f(orm), j, k"; } - } else { - bottom_text += &self.input; + } else if self.mode == Mode::FormInput && self.dimensions[0] > 500 { + bottom_text += "syntax is eg \"0,inputname=input value\""; } - instructions.push(DrawInstructions::Text([0, self.dimensions[1] - LINE_HEIGHT], vec!["nimbus-roman".to_string()], bottom_text, theme_info.text, theme_info.background, Some(1), Some(11))); + instructions.push(DrawInstructions::Text([0, self.dimensions[1] - LINE_HEIGHT * 2], vec!["nimbus-romono".to_string()], bottom_text, theme_info.top_text, theme_info.top, Some(1), Some(11))); + instructions.push(DrawInstructions::Text([0, self.dimensions[1] - LINE_HEIGHT], vec!["nimbus-romono".to_string()], self.input.clone(), theme_info.text, theme_info.background, Some(1), Some(11))); instructions } @@ -222,7 +371,7 @@ impl WindowLike for KoxingaBrowser { } fn ideal_dimensions(&self, _dimensions: Dimensions) -> Dimensions { - [500, 410] + [650, 410] } fn resizable(&self) -> bool { @@ -231,14 +380,29 @@ impl WindowLike for KoxingaBrowser { } impl KoxingaBrowser { - pub fn new() -> Self { - Default::default() + pub fn new(fonts: Vec) -> Self { + let mut s: Self = Default::default(); + s.fonts = fonts; + s } - pub fn calc_page(&mut self) { + pub fn change_url(&mut self, new_url: Url, text: String) { + self.url = Some(new_url); + self.top_line_no = 0; + self.top_level_nodes = parse(&text); + self.input = String::new(); + self.calc_page(true); + self.mode = Mode::Normal; + } + + pub fn calc_page(&mut self, new_page: bool) { self.title = None; self.page = Vec::new(); self.links = Vec::new(); + self.forms = Vec::new(); + if new_page { + self.form_inputs = HashMap::new(); + } let mut outputs = Vec::new(); if self.top_level_nodes.len() > 0 { let html_index = self.top_level_nodes.iter().position(|n| n.tag_name == "html"); @@ -247,10 +411,8 @@ impl KoxingaBrowser { if n.tag_name == "head" { //look for title, if any for hn in &n.children { - if hn.tag_name == "title" { - if hn.children[0].text_node { - self.title = Some(hn.children[0].tag_name.clone()); - } + if hn.tag_name == "title" && hn.children.len() > 0 && hn.children[0].text_node { + self.title = Some(hn.children[0].tag_name.clone()); } } } else if n.tag_name == "body" { @@ -264,11 +426,13 @@ impl KoxingaBrowser { let mut x = 2; let mut indent = 0; let mut line_count = 0; - let mut colour = false; let mut link_counter = 0; + let mut form_counter = 0; + let mut subtype = Subtype::Text; + let mut fc_getter = CachedFontCharGetter::new(81); //all eng alpha + numbers + 19 for o in outputs { //each char is width of 13 - let os = if let OutputType::Text(ref s) = o { + let output_string = if let OutputType::Text(ref s) = o { let s = if s.starts_with(" ") { " ".to_string() } else { @@ -280,7 +444,7 @@ impl KoxingaBrowser { }; Some(s) } else if let OutputType::StartLink(link) = &o { - colour = true; + subtype = Subtype::Link; if self.mode == Mode::Link { self.links.push(link.to_string()); let s = link_counter.to_string() + ":"; @@ -293,10 +457,32 @@ impl KoxingaBrowser { } else { None } + } else if let OutputType::Form(form) = &o { + //yeah, in future properly render the submit button + subtype = Subtype::Button; + self.forms.push(form.clone()); + let t = if self.mode == Mode::FormSubmit { + form_counter.to_string() + ":" + } else { + String::new() + } + "Submit Form"; + form_counter += 1; + Some(t) + } else if let OutputType::TextInput(name) = &o { + subtype = Subtype::TextInput; + if new_page { + self.form_inputs.insert((form_counter, name.to_string()), String::new()); + } + let t = if self.mode == Mode::FormInput || self.mode == Mode::FormSubmit { + format!("{},{}={}", form_counter.to_string(), name, self.form_inputs.get(&(form_counter, name.to_owned())).unwrap()) + } else { + name.to_owned() + }; + Some(t) } else { None }; - if let Some(s) = os { + if let Some(s) = output_string { //leading and trailing whitespace is probably a mistake let mut line = String::new(); if x == 2 { @@ -304,10 +490,10 @@ impl KoxingaBrowser { } let mut start_x = x; for c in s.chars() { - let c_width = measure_text(&["nimbus-roman".to_string(), "shippori-mincho".to_string()], c.to_string()).width + 1; //+1 for horiz spacing + let c_width = measure_text_with_cache(&mut fc_getter, &self.fonts, &c.to_string(), None).width + 1; //+1 for horiz spacing if x + c_width > self.dimensions[0] { //full line, add draw instruction - self.page.push((start_x, y, line, colour)); + self.page.push((start_x, y, line, subtype)); line = String::new(); x = 2 + indent; start_x = x; @@ -318,7 +504,13 @@ impl KoxingaBrowser { x += c_width; } if line.len() > 0 { - self.page.push((start_x, y, line, colour)); + self.page.push((start_x, y, line, subtype)); + } + if subtype.is_one_off() { + //so button and textinput subtypes don't persist + //really we should allow multiple subtypes at once or something, idk + //but this is fine for now + subtype = Subtype::Text; } } if let OutputType::Indent(space) = o { @@ -332,7 +524,7 @@ impl KoxingaBrowser { y += LINE_HEIGHT; line_count += 1; } else if o == OutputType::EndLink { - colour = false; + subtype = Subtype::Text; } } self.max_lines = line_count; @@ -340,5 +532,5 @@ impl KoxingaBrowser { } pub fn main() { - listen(KoxingaBrowser::new()); + listen(KoxingaBrowser::new(vec!["nimbus-roman".to_string(), "shippori-mincho".to_string()])); } diff --git a/src/url.rs b/src/url.rs new file mode 100644 index 0000000..efe52b7 --- /dev/null +++ b/src/url.rs @@ -0,0 +1,71 @@ +use std::vec::Vec; +use std::fmt; + +//for the moment, we don't care about query params or fragments and the like +#[derive(Clone)] +pub struct Url { + scheme: String, //http or https, probably + hostname: String, + path: Vec, + query: Option, //empty or somethign like ?value1=yes&value2=abcd +} + +impl fmt::Display for Url { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + if self.scheme != "" { + fmt.write_str(&(self.scheme.clone() + "://" + &self.hostname + "/" + &self.path.join("/") + &self.query.as_ref().map_or("", |v| v)))?; + } + Ok(()) + } +} + +impl Url { + pub fn new(url: String) -> Url { + let mut queries = url.split("?"); + let mut p = queries.next().unwrap().split("://"); + let scheme = p.next().unwrap_or("").to_string(); + p = p.next().unwrap_or("").split("/"); + let hostname = p.next().unwrap_or("").to_string(); + let path = p.filter(|s| *s != "").map(|s| s.to_string()).collect(); + let query = match queries.next() { + Some(q) => Some(q.to_string()), + None => None, + }; + Self { scheme, hostname, path, query } + } + + pub fn new_maybe_relative(url: String, current_url: Url) -> Url { + if url.split("://").count() < 2 { + let mut use_url = current_url; + if url.starts_with("/") { + use_url.pop_to_root(); + }; + use_url.append(url); + use_url + } else { + Url::new(url) + } + } + + pub fn pop(&mut self) { + self.path.pop(); + self.query = None; + } + + pub fn pop_to_root(&mut self) { + self.path = Vec::new(); + self.query = None; + } + + pub fn append(&mut self, path: String) { + self.path.extend(path.split("/").filter(|s| *s != "").map(|s| s.to_string())); + } + + pub fn append_query(&mut self, key: &str, value: &str) { + if self.query.is_none() { + self.query = Some(format!("?{}={}", key, value)); + } else { + self.query = Some(format!("{}&{}={}", self.query.as_ref().unwrap(), key, value)); + } + } +} diff --git a/src/xml.rs b/src/xml.rs index 8bb7df0..3396626 100644 --- a/src/xml.rs +++ b/src/xml.rs @@ -4,6 +4,8 @@ use std::collections::HashMap; use ming_wm_lib::utils::Substring; +//use ming_wm_lib::logging::log; + //try to be xhtml compliant? //fuck these mfers. self close with a FUCKING slash man. @@ -34,14 +36,39 @@ fn handle_escaped(s: &str) -> String { s } +pub fn remove_quotes(s: String) -> String { + //todo: remove only if quotes + let s_len = s.len(); + if s_len > 1 { + s.substring(1, s.len() - 1).to_string() + } else { + s //length is 0 or 1, can't strip no quotes... + } +} + +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum FormSubmitMethod { + Get, + Post, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Form { + pub action: String, //url + pub method: FormSubmitMethod, + pub input_names: Vec, +} + #[derive(Debug, PartialEq)] pub enum OutputType { - StartLink(String), + StartLink(String), //url EndLink, Text(String), Newline, //only support one per line, once indented, will keep being indented until overriden, for now Indent(usize), + TextInput(String), + Form(Form), } #[derive(Clone, Default, Debug, PartialEq)] @@ -57,7 +84,11 @@ impl Node { pub fn to_output(&self) -> Vec { let mut output = Vec::new(); let mut link = false; - if self.text_node { + let mut form = None; + let mut input_names = Vec::new(); + if Some(&"\"true\"".to_string()) == self.attributes.get("aria-hidden") { + return output; + } else if self.text_node { output.push(OutputType::Text(handle_escaped(&self.tag_name.clone()))); return output; } else if self.tag_name == "script" || self.tag_name == "style" { @@ -70,25 +101,68 @@ impl Node { output.push(OutputType::StartLink(href.to_string())); } else if let Some(indent) = self.attributes.get("indent") { //non-standard indent attribute, basically just to support HN - //remove quotes - let indent = indent.substring(1, indent.len() - 1); + let indent = remove_quotes(indent.to_string()); if let Ok(indent) = indent.parse::() { output.push(OutputType::Indent(indent * 32)); } + } else if self.tag_name == "input" || self.tag_name == "textarea" { + if let Some(name) = self.attributes.get("name") { + //unwrap_or is painful so compiler suggested map_or + let input_type = remove_quotes(self.attributes.get("type").map_or("\"text\"".to_string(), |v| v.to_string())); + if input_type == "text" || input_type == "search" { + output.push(OutputType::TextInput(remove_quotes(name.to_string()))); + } + } + } else if self.tag_name == "form" { + if let Some(action) = self.attributes.get("action") { + let method = if let Some(m) = self.attributes.get("method") { + let m = remove_quotes(m.to_string()).to_lowercase(); + match m.as_str() { + //todo: POST is currently not implemented. maybe later + //"post" => Some(FormSubmitMethod::Post), + "get" => Some(FormSubmitMethod::Get), + _ => None, + } + } else { + Some(FormSubmitMethod::Get) + }; + if let Some(method) = method { + form = Some(Form { + action: remove_quotes(action.to_string()), + method, + input_names: Vec::new(), + }); + } + } } for c in &self.children { - output.extend(c.to_output()); + let children_output = c.to_output(); + if form.is_some() { + for cc in &children_output { + if let OutputType::TextInput(name) = cc { + input_names.push(name.to_string()); + } + } + } + output.extend(children_output); } if BLOCK_LEVEL.contains(&self.tag_name.as_str()) { output.push(OutputType::Newline); } else if link { output.push(OutputType::EndLink); + } else if let Some(form) = form { + let form = Form { + action: form.action, + method: form.method, + input_names, + }; + output.push(OutputType::Form(form)); } output } } -fn add_to_parent(top_level_nodes: &mut Vec>, parent_location: &Vec, node: Node) -> usize { +fn add_to_parent(top_level_nodes: &mut Vec>, parent_location: &[usize], node: Node) -> usize { if parent_location.len() == 0 { top_level_nodes.push(Box::new(node)); top_level_nodes.len() - 1 @@ -157,9 +231,7 @@ pub fn parse(xml_string: &str) -> Vec> { let end = so_far.chars().count(); if so_far.substring(end - end_len, end) == "" { current_node = None; - let mut n2: Node = Default::default(); - n2.text_node = true; - n2.tag_name = so_far.substring(0, end - end_len).to_string(); + let n2: Node = Node { text_node: true, tag_name: so_far.substring(0, end - end_len).to_string(), ..Default::default() }; add_to_parent(&mut top_level_nodes, &parent_location, n2); parent_location.pop(); recording_tag_name = false; @@ -167,7 +239,7 @@ pub fn parse(xml_string: &str) -> Vec> { } } } - } else if c == ' ' && recording_tag_name && !n.text_node { + } else if (c == ' ' || c == '\n') && recording_tag_name && !n.text_node { recording_tag_name = false; } else if c == '>' || (c == '/' && chars.peek().unwrap_or(&' ') == &'>') || (n.text_node && chars.peek().unwrap_or(&' ') == &'<') { if n.text_node { @@ -189,12 +261,14 @@ pub fn parse(xml_string: &str) -> Vec> { current_node = None; } else if recording_tag_name { n.tag_name += &c.to_string(); - } else if c == ' ' && !in_string && recording_attribute_value { + } else if c == ' ' && !in_string { //catch attributes like disabled with no = or value if attribute_name.len() > 0 && !recording_attribute_value { n.attributes.entry(attribute_name.clone()).insert_entry(String::new()); + } else if recording_attribute_value { + //^this can just be an "else", not an "else if", probably + recording_attribute_value = false; } - recording_attribute_value = false; attribute_name = String::new(); } else if recording_attribute_value { if c == '"' { @@ -215,9 +289,7 @@ pub fn parse(xml_string: &str) -> Vec> { //skip the rest of the loop { let c2 = chars.next(); - if c2.is_none() { - break; - } else if c2.unwrap() == '>' { + if c2.is_none() || c2.unwrap() == '>' { break; } } @@ -232,9 +304,7 @@ pub fn parse(xml_string: &str) -> Vec> { whitespace_only = false; } //text node - let mut n: Node = Default::default(); - n.tag_name = c.to_string(); - n.text_node = true; + let n: Node = Node { tag_name: c.to_string(), text_node: true, ..Default::default() }; if chars.peek().unwrap_or(&' ') == &'<' { add_to_parent(&mut top_level_nodes, &parent_location, n); } else { @@ -301,6 +371,42 @@ fn test_comments_xml_parse() { assert!(nodes[1].children[0].tag_name == " afterwards"); } +#[test] +fn test_weird_attr() { + //weird order + let nodes = parse(""); + assert!(nodes[0].attributes.get("type").unwrap() == "\"text\""); + assert!(nodes[0].attributes.get("disabled").is_some()); + assert!(nodes[0].attributes.get("name").unwrap() == "\"one\""); + //newlines in tag and shit + let nodes = parse(""); + assert!(nodes[0].tag_name == "input"); + //assert!(nodes[0].attributes.get("title").unwrap() == "\"invalid text\"\n"); //current has newline at end I think (TODO: fix) + // + assert!(nodes[0].attributes.get("name").unwrap() == "\"one\""); +} + +#[test] +fn test_form_parse_and_output() { + let nodes = parse(" +
+ test + yeah +"); + assert!(nodes.len() == 1); + assert!(nodes[0].tag_name == "form"); + assert!(nodes[0].children[0].children[0].tag_name == "input"); + assert!(nodes[0].children[1].tag_name == "label"); + assert!(nodes[0].children[3].tag_name == "input"); + assert!(nodes[0].children[4].tag_name == "label"); + //check .to_output() + // +} + + /*#[test] fn test_real() { use std::fs::read_to_string; @@ -309,5 +415,3 @@ fn test_real() { println!("{:?}", nodes[1].children[1].to_output()); println!("{}", nodes[1].children[1].tag_name); }*/ - -//more tests 100% needed. yoink from news.ycombinator.com and en.wikipedia.org