hn front page and fanclub working
This commit is contained in:
10
src/http.rs
Normal file
10
src/http.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use reqwest::blocking;
|
||||
|
||||
pub fn get(url: &str) -> Option<String> {
|
||||
if let Ok(resp) = blocking::get(url) {
|
||||
if let Ok(text) = resp.text() {
|
||||
return Some(text);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
191
src/main.rs
Normal file
191
src/main.rs
Normal file
@@ -0,0 +1,191 @@
|
||||
use std::vec::Vec;
|
||||
use std::vec;
|
||||
use std::fmt;
|
||||
use std::boxed::Box;
|
||||
|
||||
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::themes::ThemeInfo;
|
||||
use ming_wm_lib::ipc::listen;
|
||||
|
||||
mod http;
|
||||
use crate::http::get;
|
||||
mod xml;
|
||||
use crate::xml::{ parse, Node, OutputType };
|
||||
|
||||
const LINE_HEIGHT: usize = 18;
|
||||
|
||||
#[derive(Default, PartialEq)]
|
||||
enum Mode {
|
||||
#[default]
|
||||
Normal,
|
||||
Url,
|
||||
}
|
||||
|
||||
impl fmt::Display for Mode {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
fmt.write_str(match self {
|
||||
Mode::Normal => "NORMAL",
|
||||
Mode::Url => "URL",
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct KoxingaBrowser {
|
||||
dimensions: Dimensions,
|
||||
mode: Mode,
|
||||
top_line_no: usize,
|
||||
url_input: String,
|
||||
top_level_nodes: Vec<Box<Node>>,
|
||||
}
|
||||
|
||||
impl WindowLike for KoxingaBrowser {
|
||||
fn handle_message(&mut self, message: WindowMessage) -> WindowMessageResponse {
|
||||
match message {
|
||||
WindowMessage::Init(dimensions) => {
|
||||
self.dimensions = dimensions;
|
||||
WindowMessageResponse::JustRedraw
|
||||
},
|
||||
WindowMessage::ChangeDimensions(dimensions) => {
|
||||
self.dimensions = dimensions;
|
||||
WindowMessageResponse::JustRedraw
|
||||
},
|
||||
WindowMessage::KeyPress(key_press) => {
|
||||
match self.mode {
|
||||
Mode::Normal => {
|
||||
if key_press.key == 'u' {
|
||||
self.mode = Mode::Url;
|
||||
WindowMessageResponse::JustRedraw
|
||||
} else if key_press.key == 'k' {
|
||||
if self.top_line_no > 0 {
|
||||
self.top_line_no -= 1;
|
||||
}
|
||||
WindowMessageResponse::JustRedraw
|
||||
} else if key_press.key == 'j' {
|
||||
self.top_line_no += 1;
|
||||
WindowMessageResponse::JustRedraw
|
||||
} else {
|
||||
WindowMessageResponse::DoNothing
|
||||
}
|
||||
},
|
||||
Mode::Url => {
|
||||
if key_press.key == '𐘂' { //the enter key
|
||||
if let Some(text) = get(&self.url_input) {
|
||||
self.top_line_no = 0;
|
||||
self.top_level_nodes = parse(&text);
|
||||
self.mode = Mode::Normal;
|
||||
}
|
||||
} else if key_press.key == '𐘃' { //escape key
|
||||
self.mode = Mode::Normal;
|
||||
} else if key_press.key == '𐘁' { //backspace
|
||||
if self.url_input.len() > 0 {
|
||||
self.url_input = self.url_input.remove_last();
|
||||
} else {
|
||||
return WindowMessageResponse::DoNothing;
|
||||
}
|
||||
} else {
|
||||
self.url_input += &key_press.key.to_string();
|
||||
}
|
||||
WindowMessageResponse::JustRedraw
|
||||
},
|
||||
}
|
||||
},
|
||||
_ => WindowMessageResponse::DoNothing,
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(&self, theme_info: &ThemeInfo) -> Vec<DrawInstructions> {
|
||||
let mut instructions = Vec::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");
|
||||
if let Some(html_index) = html_index {
|
||||
for n in &self.top_level_nodes[html_index].children {
|
||||
if n.tag_name == "body" {
|
||||
outputs = n.to_output();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut y = 2;
|
||||
let mut x = 2;
|
||||
let mut colour = theme_info.text;
|
||||
for o in outputs {
|
||||
//each char is width of 13
|
||||
match o {
|
||||
OutputType::Text(s) => {
|
||||
//leading and trailing whitespace is probably a mistake
|
||||
let s = s.trim();
|
||||
let mut line = String::new();
|
||||
let mut start_x = x;
|
||||
for c in s.chars() {
|
||||
if x + 14 > self.dimensions[0] {
|
||||
//full line, add draw instruction
|
||||
let line_no = (y - 2) / LINE_HEIGHT;
|
||||
if line_no >= self.top_line_no {
|
||||
instructions.push(DrawInstructions::Text([start_x, y - LINE_HEIGHT * self.top_line_no], vec!["nimbus-roman".to_string()], line, colour, theme_info.background, Some(1), Some(14)));
|
||||
}
|
||||
line = String::new();
|
||||
x = 2;
|
||||
start_x = x;
|
||||
y += LINE_HEIGHT;
|
||||
}
|
||||
line += &c.to_string();
|
||||
x += 14;
|
||||
}
|
||||
let line_no = (y - 2) / LINE_HEIGHT;
|
||||
if line.len() > 0 && line_no >= self.top_line_no {
|
||||
instructions.push(DrawInstructions::Text([start_x, y - LINE_HEIGHT * self.top_line_no], vec!["nimbus-roman".to_string()], line, colour, theme_info.background, Some(1), Some(14)));
|
||||
}
|
||||
},
|
||||
OutputType::Newline => {
|
||||
x = 2;
|
||||
y += LINE_HEIGHT;
|
||||
},
|
||||
OutputType::StartLink(_) => {
|
||||
colour = theme_info.top_text;
|
||||
},
|
||||
OutputType::EndLink => {
|
||||
colour = theme_info.text;
|
||||
},
|
||||
};
|
||||
}
|
||||
//mode
|
||||
let mut bottom_text = self.mode.to_string() + ": ";
|
||||
if self.mode == Mode::Url {
|
||||
bottom_text += &self.url_input;
|
||||
}
|
||||
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(12)));
|
||||
instructions
|
||||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
"Koxinga Browser".to_string()
|
||||
}
|
||||
|
||||
fn subtype(&self) -> WindowLikeType {
|
||||
WindowLikeType::Window
|
||||
}
|
||||
|
||||
fn ideal_dimensions(&self, _dimensions: Dimensions) -> Dimensions {
|
||||
[410, 410]
|
||||
}
|
||||
|
||||
fn resizable(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl KoxingaBrowser {
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main() {
|
||||
listen(KoxingaBrowser::new());
|
||||
}
|
||||
205
src/xml.rs
Normal file
205
src/xml.rs
Normal file
@@ -0,0 +1,205 @@
|
||||
use std::vec::Vec;
|
||||
use std::boxed::Box;
|
||||
use std::collections::HashMap;
|
||||
|
||||
//try to be xhtml compliant?
|
||||
|
||||
//fuck these mfers. self close with a FUCKING slash man.
|
||||
//<meta> is bad, <meta/> is good!!
|
||||
const SELF_CLOSING: [&'static str; 7] = ["link", "meta", "input", "img", "br", "hr", "!DOCTYPE"];
|
||||
|
||||
fn is_whitespace(c: char) -> bool {
|
||||
c == ' ' || c == '\x09'
|
||||
}
|
||||
|
||||
pub enum OutputType {
|
||||
StartLink(String),
|
||||
EndLink,
|
||||
Text(String),
|
||||
Newline,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug, PartialEq)]
|
||||
pub struct Node {
|
||||
//for text nodes, tag_name is the content
|
||||
pub tag_name: String,
|
||||
pub attributes: HashMap<String, String>,
|
||||
pub children: Vec<Box<Node>>,
|
||||
pub text_node: bool,
|
||||
}
|
||||
|
||||
impl Node {
|
||||
pub fn to_output(&self) -> Vec<OutputType> {
|
||||
let mut output = Vec::new();
|
||||
let mut link = false;
|
||||
if self.text_node {
|
||||
output.push(OutputType::Text(self.tag_name.clone()));
|
||||
return output;
|
||||
} else if self.tag_name == "script" || self.tag_name == "style" {
|
||||
//ignore script and style tags
|
||||
return output;
|
||||
} else if let Some(href) = self.attributes.get("href") {
|
||||
link = true;
|
||||
output.push(OutputType::StartLink(href.to_string()));
|
||||
}
|
||||
for c in &self.children {
|
||||
output.extend(c.to_output());
|
||||
}
|
||||
if self.tag_name == "p" || self.tag_name == "br" || self.tag_name == "li" || self.tag_name == "tr" {
|
||||
output.push(OutputType::Newline);
|
||||
} else if link {
|
||||
output.push(OutputType::EndLink);
|
||||
}
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
fn add_to_parent(top_level_nodes: &mut Vec<Box<Node>>, parent_location: &Vec<usize>, node: Node) -> usize {
|
||||
if parent_location.len() == 0 {
|
||||
top_level_nodes.push(Box::new(node));
|
||||
top_level_nodes.len() - 1
|
||||
} else {
|
||||
let mut parent_children = &mut top_level_nodes[parent_location[0]].children;
|
||||
for i in &parent_location[1..] {
|
||||
parent_children = &mut parent_children[*i].children;
|
||||
}
|
||||
let loc = parent_children.len();
|
||||
parent_children.push(Box::new(node));
|
||||
loc
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(xml_string: &str) -> Vec<Box<Node>> {
|
||||
let mut top_level_nodes = Vec::new();
|
||||
let mut chars = xml_string.chars().peekable();
|
||||
let mut parent_location: Vec<usize> = Vec::new(); //vec of indexes
|
||||
let mut recording_tag_name = false;
|
||||
let mut whitespace_only = true; //ignore leading whitespace on each line
|
||||
let mut attribute_name = String::new();
|
||||
let mut recording_attribute_value = false;
|
||||
let mut in_string = false;
|
||||
let mut current_node: Option<Node> = None;
|
||||
loop {
|
||||
let c = chars.next();
|
||||
if c.is_none() {
|
||||
break;
|
||||
}
|
||||
let c = c.unwrap();
|
||||
if let Some(ref mut n) = current_node {
|
||||
if c == ' ' && 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 {
|
||||
n.tag_name += &c.to_string();
|
||||
}
|
||||
let loc = add_to_parent(&mut top_level_nodes, &parent_location, n.clone());
|
||||
if c == '>' && !SELF_CLOSING.contains(&n.tag_name.as_str()) {
|
||||
parent_location.push(loc);
|
||||
} else if c == '/' {
|
||||
chars.next();
|
||||
}
|
||||
//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());
|
||||
}
|
||||
recording_tag_name = false;
|
||||
recording_attribute_value = false;
|
||||
attribute_name = String::new();
|
||||
current_node = None;
|
||||
} else if recording_tag_name {
|
||||
n.tag_name += &c.to_string();
|
||||
} else if c == ' ' && !in_string && recording_attribute_value {
|
||||
//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());
|
||||
}
|
||||
recording_attribute_value = false;
|
||||
attribute_name = String::new();
|
||||
} else if recording_attribute_value {
|
||||
if c == '"' {
|
||||
in_string = *n.attributes.get(&attribute_name).unwrap() == "";
|
||||
}
|
||||
n.attributes.entry(attribute_name.clone()).and_modify(|s| *s += &c.to_string());
|
||||
} else if c == '=' {
|
||||
n.attributes.entry(attribute_name.clone()).insert_entry(String::new());
|
||||
recording_attribute_value = true;
|
||||
} else {
|
||||
attribute_name += &c.to_string();
|
||||
}
|
||||
//todo: record attributes
|
||||
} else if c == '<' {
|
||||
whitespace_only = false;
|
||||
if chars.peek().unwrap_or(&' ') == &'/' {
|
||||
parent_location.pop();
|
||||
//skip the rest of the </ >
|
||||
loop {
|
||||
let c2 = chars.next();
|
||||
if c2.is_none() {
|
||||
break;
|
||||
} else if c2.unwrap() == '>' {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
current_node = Some(Default::default());
|
||||
recording_tag_name = true;
|
||||
}
|
||||
} else if c == '\n' {
|
||||
whitespace_only = true;
|
||||
} else if !is_whitespace(c) || !whitespace_only {
|
||||
if !is_whitespace(c) {
|
||||
whitespace_only = false;
|
||||
}
|
||||
//text node
|
||||
let mut n: Node = Default::default();
|
||||
n.tag_name = c.to_string();
|
||||
n.text_node = true;
|
||||
if chars.peek().unwrap_or(&' ') == &'<' {
|
||||
add_to_parent(&mut top_level_nodes, &parent_location, n);
|
||||
} else {
|
||||
recording_tag_name = true;
|
||||
current_node = Some(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
top_level_nodes
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_xml_parse() {
|
||||
let nodes = parse("<p>Woah <span id=\"spanner\">lorem ipsum</span> !!! no way</p>
|
||||
<input name=\"in put\" disabled/>
|
||||
<div>
|
||||
<a href=\"https://wikipedia.org\" title=12>Wikipedia</a>
|
||||
<p>Nested woah <b>woah</b></p>
|
||||
</div>");
|
||||
assert!(nodes.len() == 3);
|
||||
assert!(nodes[0].tag_name == "p");
|
||||
assert!(nodes[0].children.len() == 3);
|
||||
assert!(nodes[0].children[0].tag_name == "Woah ");
|
||||
assert!(nodes[0].children[1].tag_name == "span");
|
||||
assert!(nodes[0].children[1].children[0].tag_name == "lorem ipsum");
|
||||
assert!(nodes[0].children[2].tag_name == " !!! no way");
|
||||
assert!(nodes[1].tag_name == "input");
|
||||
assert!(nodes[1].attributes.get("name").unwrap() == "\"in put\"");
|
||||
assert!(nodes[2].tag_name == "div");
|
||||
assert!(nodes[2].children.len() == 2);
|
||||
assert!(nodes[2].children[0].tag_name == "a");
|
||||
assert!(nodes[2].children[0].attributes.get("href").unwrap() == "\"https://wikipedia.org\"");
|
||||
assert!(nodes[2].children[0].attributes.get("title").unwrap() == "12");
|
||||
assert!(nodes[2].children[0].children[0].tag_name == "Wikipedia");
|
||||
assert!(nodes[2].children[1].tag_name == "p");
|
||||
assert!(nodes[2].children[1].children[0].tag_name == "Nested woah ");
|
||||
assert!(nodes[2].children[1].children[1].tag_name == "b");
|
||||
assert!(nodes[2].children[1].children[1].children[0].tag_name == "woah");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_close_xml_parse() {
|
||||
let nodes = parse("<span> (<a>Hey</a>Woah)</span>");
|
||||
assert!(nodes[0].children[1].tag_name == "a");
|
||||
let nodes = parse("<a>ab</a> <span>woah</span>");
|
||||
assert!(nodes[2].tag_name == "span");
|
||||
}
|
||||
|
||||
//more tests 100% needed. yoink from news.ycombinator.com and en.wikipedia.org
|
||||
Reference in New Issue
Block a user