hn front page and fanclub working

This commit is contained in:
stjet
2025-03-05 09:31:42 +00:00
commit f31dd48a87
9 changed files with 2626 additions and 0 deletions

10
src/http.rs Normal file
View 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
View 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
View 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