Text measuring, HN comment indenting
4
Cargo.lock
generated
@@ -604,9 +604,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ming-wm-lib"
|
name = "ming-wm-lib"
|
||||||
version = "0.1.5"
|
version = "0.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9d90e1d57dcc9ff559f34d885a0c62e86ef881b4371e3f3b02c909460d3454b5"
|
checksum = "2cc80b6035509629ecba931bc6851513fca9fa8bef2be965b8bd437dd00b3930"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "koxinga"
|
name = "koxinga"
|
||||||
version = "0.1.1"
|
version = "0.2.0-beta.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
@@ -8,5 +8,5 @@ name = "mingInternet_Koxinga_Browser"
|
|||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ming-wm-lib = "0.1.5"
|
ming-wm-lib = "0.2.2"
|
||||||
reqwest = { version = "0.12", features = [ "blocking" ] }
|
reqwest = { version = "0.12", features = [ "blocking" ] }
|
||||||
|
|||||||
BIN
koxinga_hn.png
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 122 KiB |
BIN
koxinga_wiki.png
|
Before Width: | Height: | Size: 294 KiB After Width: | Height: | Size: 285 KiB |
|
Before Width: | Height: | Size: 196 KiB After Width: | Height: | Size: 168 KiB |
BIN
old_koxinga_wiki.png
Normal file
|
After Width: | Height: | Size: 294 KiB |
BIN
old_koxinga_within.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
882
real_tests/wikipedia.html
Normal file
23
src/http.rs
@@ -1,10 +1,27 @@
|
|||||||
use reqwest::blocking;
|
use reqwest::blocking::Client;
|
||||||
|
|
||||||
pub fn get(url: &str) -> Option<String> {
|
//for now, just a thin wrapper
|
||||||
if let Ok(resp) = blocking::get(url) {
|
pub struct HttpClient {
|
||||||
|
client: Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::default::Default for HttpClient {
|
||||||
|
fn default() -> Self {
|
||||||
|
//for privacy can change to more common one
|
||||||
|
let client = Client::builder().user_agent("Koxinga").build().unwrap();
|
||||||
|
Self {
|
||||||
|
client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HttpClient {
|
||||||
|
pub fn get(&self, url: &str) -> Option<String> {
|
||||||
|
if let Ok(resp) = self.client.get(url).send() {
|
||||||
if let Ok(text) = resp.text() {
|
if let Ok(text) = resp.text() {
|
||||||
return Some(text);
|
return Some(text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
66
src/main.rs
@@ -3,15 +3,17 @@ use std::vec;
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::boxed::Box;
|
use std::boxed::Box;
|
||||||
|
|
||||||
|
//use ming_wm_lib::logging::log;
|
||||||
use ming_wm_lib::window_manager_types::{ DrawInstructions, WindowLike, WindowLikeType };
|
use ming_wm_lib::window_manager_types::{ DrawInstructions, WindowLike, WindowLikeType };
|
||||||
use ming_wm_lib::messages::{ WindowMessage, WindowMessageResponse };
|
use ming_wm_lib::messages::{ WindowMessage, WindowMessageResponse };
|
||||||
use ming_wm_lib::utils::Substring;
|
use ming_wm_lib::utils::Substring;
|
||||||
use ming_wm_lib::framebuffer_types::Dimensions;
|
use ming_wm_lib::framebuffer_types::Dimensions;
|
||||||
use ming_wm_lib::themes::ThemeInfo;
|
use ming_wm_lib::themes::ThemeInfo;
|
||||||
|
use ming_wm_lib::fonts::measure_text;
|
||||||
use ming_wm_lib::ipc::listen;
|
use ming_wm_lib::ipc::listen;
|
||||||
|
|
||||||
mod http;
|
mod http;
|
||||||
use crate::http::get;
|
use crate::http::HttpClient;
|
||||||
mod xml;
|
mod xml;
|
||||||
use crate::xml::{ parse, Node, OutputType };
|
use crate::xml::{ parse, Node, OutputType };
|
||||||
|
|
||||||
@@ -55,6 +57,7 @@ impl fmt::Display for Mode {
|
|||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct KoxingaBrowser {
|
struct KoxingaBrowser {
|
||||||
|
client: HttpClient,
|
||||||
dimensions: Dimensions,
|
dimensions: Dimensions,
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
max_lines: usize,
|
max_lines: usize,
|
||||||
@@ -62,6 +65,7 @@ struct KoxingaBrowser {
|
|||||||
url: Option<String>,
|
url: Option<String>,
|
||||||
input: String,
|
input: String,
|
||||||
links: Vec<String>,
|
links: Vec<String>,
|
||||||
|
title: Option<String>,
|
||||||
top_level_nodes: Vec<Box<Node>>,
|
top_level_nodes: Vec<Box<Node>>,
|
||||||
page: Vec<(usize, usize, String, bool)>, //x, y, text, link colour or not
|
page: Vec<(usize, usize, String, bool)>, //x, y, text, link colour or not
|
||||||
}
|
}
|
||||||
@@ -81,7 +85,7 @@ impl WindowLike for KoxingaBrowser {
|
|||||||
WindowMessage::KeyPress(key_press) => {
|
WindowMessage::KeyPress(key_press) => {
|
||||||
match self.mode {
|
match self.mode {
|
||||||
Mode::Normal => {
|
Mode::Normal => {
|
||||||
let max_lines_screen = (self.dimensions[1] - 4) / LINE_HEIGHT - 1;
|
let max_lines_screen = (self.dimensions[1] - 2) / LINE_HEIGHT - 1;
|
||||||
if key_press.key == 'u' {
|
if key_press.key == 'u' {
|
||||||
self.mode = Mode::Url;
|
self.mode = Mode::Url;
|
||||||
self.input = self.url.clone().unwrap_or(String::new());
|
self.input = self.url.clone().unwrap_or(String::new());
|
||||||
@@ -99,7 +103,7 @@ impl WindowLike for KoxingaBrowser {
|
|||||||
}
|
}
|
||||||
WindowMessageResponse::JustRedraw
|
WindowMessageResponse::JustRedraw
|
||||||
} else if key_press.key == 'j' {
|
} else if key_press.key == 'j' {
|
||||||
if self.top_line_no < self.max_lines - max_lines_screen {
|
if self.top_line_no < self.max_lines - max_lines_screen + 1 {
|
||||||
self.top_line_no += 1;
|
self.top_line_no += 1;
|
||||||
WindowMessageResponse::JustRedraw
|
WindowMessageResponse::JustRedraw
|
||||||
} else {
|
} else {
|
||||||
@@ -109,7 +113,7 @@ impl WindowLike for KoxingaBrowser {
|
|||||||
self.top_line_no = 0;
|
self.top_line_no = 0;
|
||||||
WindowMessageResponse::JustRedraw
|
WindowMessageResponse::JustRedraw
|
||||||
} else if key_press.key == 'G' {
|
} else if key_press.key == 'G' {
|
||||||
self.top_line_no = self.max_lines - max_lines_screen;
|
self.top_line_no = self.max_lines - max_lines_screen + 1;
|
||||||
WindowMessageResponse::JustRedraw
|
WindowMessageResponse::JustRedraw
|
||||||
} else {
|
} else {
|
||||||
WindowMessageResponse::DoNothing
|
WindowMessageResponse::DoNothing
|
||||||
@@ -120,6 +124,7 @@ impl WindowLike for KoxingaBrowser {
|
|||||||
let new_url = if self.mode == Mode::Search {
|
let new_url = if self.mode == Mode::Search {
|
||||||
"https://old-search.marginalia.nu/search?query=".to_string() + &self.input
|
"https://old-search.marginalia.nu/search?query=".to_string() + &self.input
|
||||||
} else if self.mode == Mode::Link {
|
} else if self.mode == Mode::Link {
|
||||||
|
self.mode = Mode::Normal;
|
||||||
let link_index = self.input.parse::<usize>().unwrap();
|
let link_index = self.input.parse::<usize>().unwrap();
|
||||||
let url = self.url.as_ref().unwrap();
|
let url = self.url.as_ref().unwrap();
|
||||||
let mut link;
|
let mut link;
|
||||||
@@ -142,9 +147,10 @@ impl WindowLike for KoxingaBrowser {
|
|||||||
//if Mode::Url
|
//if Mode::Url
|
||||||
self.input.clone()
|
self.input.clone()
|
||||||
};
|
};
|
||||||
if let Some(text) = get(&new_url) {
|
if let Some(text) = self.client.get(&new_url) {
|
||||||
self.url = Some(new_url.clone());
|
self.url = Some(new_url.clone());
|
||||||
self.top_line_no = 0;
|
self.top_line_no = 0;
|
||||||
|
//log(&text);
|
||||||
self.top_level_nodes = parse(&text);
|
self.top_level_nodes = parse(&text);
|
||||||
self.input = String::new();
|
self.input = String::new();
|
||||||
self.calc_page();
|
self.calc_page();
|
||||||
@@ -179,11 +185,13 @@ impl WindowLike for KoxingaBrowser {
|
|||||||
|
|
||||||
fn draw(&self, theme_info: &ThemeInfo) -> Vec<DrawInstructions> {
|
fn draw(&self, theme_info: &ThemeInfo) -> Vec<DrawInstructions> {
|
||||||
let mut instructions = Vec::new();
|
let mut instructions = Vec::new();
|
||||||
let max_lines_screen = (self.dimensions[1] - 4) / LINE_HEIGHT - 1;
|
let max_lines_screen = (self.dimensions[1] - 2) / LINE_HEIGHT - 1;
|
||||||
for p in &self.page {
|
for p in &self.page {
|
||||||
let line_no = (p.1 - 2) / LINE_HEIGHT;
|
let line_no = (p.1 - 2) / LINE_HEIGHT;
|
||||||
if line_no >= self.top_line_no && line_no < self.top_line_no + max_lines_screen {
|
if 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), Some(11)));
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//mode
|
//mode
|
||||||
@@ -201,7 +209,12 @@ impl WindowLike for KoxingaBrowser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn title(&self) -> String {
|
fn title(&self) -> String {
|
||||||
"Koxinga Browser".to_string()
|
let t = if let Some(title) = &self.title {
|
||||||
|
format!(": {}", title)
|
||||||
|
} else {
|
||||||
|
" Browser".to_string()
|
||||||
|
};
|
||||||
|
"Koxinga".to_string() + &t
|
||||||
}
|
}
|
||||||
|
|
||||||
fn subtype(&self) -> WindowLikeType {
|
fn subtype(&self) -> WindowLikeType {
|
||||||
@@ -223,6 +236,7 @@ impl KoxingaBrowser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn calc_page(&mut self) {
|
pub fn calc_page(&mut self) {
|
||||||
|
self.title = None;
|
||||||
self.page = Vec::new();
|
self.page = Vec::new();
|
||||||
self.links = Vec::new();
|
self.links = Vec::new();
|
||||||
let mut outputs = Vec::new();
|
let mut outputs = Vec::new();
|
||||||
@@ -230,14 +244,26 @@ impl KoxingaBrowser {
|
|||||||
let html_index = self.top_level_nodes.iter().position(|n| n.tag_name == "html");
|
let html_index = self.top_level_nodes.iter().position(|n| n.tag_name == "html");
|
||||||
if let Some(html_index) = html_index {
|
if let Some(html_index) = html_index {
|
||||||
for n in &self.top_level_nodes[html_index].children {
|
for n in &self.top_level_nodes[html_index].children {
|
||||||
if n.tag_name == "body" {
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if n.tag_name == "body" {
|
||||||
outputs = n.to_output();
|
outputs = n.to_output();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut y = 2;
|
let mut y = 2;
|
||||||
let mut x = 2;
|
let mut x = 2;
|
||||||
|
let mut indent = 0;
|
||||||
|
let mut line_count = 0;
|
||||||
let mut colour = false;
|
let mut colour = false;
|
||||||
let mut link_counter = 0;
|
let mut link_counter = 0;
|
||||||
for o in outputs {
|
for o in outputs {
|
||||||
@@ -273,31 +299,43 @@ impl KoxingaBrowser {
|
|||||||
if let Some(s) = os {
|
if let Some(s) = os {
|
||||||
//leading and trailing whitespace is probably a mistake
|
//leading and trailing whitespace is probably a mistake
|
||||||
let mut line = String::new();
|
let mut line = String::new();
|
||||||
|
if x == 2 {
|
||||||
|
x += indent;
|
||||||
|
}
|
||||||
let mut start_x = x;
|
let mut start_x = x;
|
||||||
for c in s.chars() {
|
for c in s.chars() {
|
||||||
if x + 12 > self.dimensions[0] {
|
let c_width = measure_text(&["nimbus-roman".to_string(), "shippori-mincho".to_string()], c.to_string()).width + 1; //+1 for horiz spacing
|
||||||
|
if x + c_width > self.dimensions[0] {
|
||||||
//full line, add draw instruction
|
//full line, add draw instruction
|
||||||
self.page.push((start_x, y, line, colour));
|
self.page.push((start_x, y, line, colour));
|
||||||
line = String::new();
|
line = String::new();
|
||||||
x = 2;
|
x = 2 + indent;
|
||||||
start_x = x;
|
start_x = x;
|
||||||
y += LINE_HEIGHT;
|
y += LINE_HEIGHT;
|
||||||
|
line_count += 1;
|
||||||
}
|
}
|
||||||
line += &c.to_string();
|
line += &c.to_string();
|
||||||
x += 12;
|
x += c_width;
|
||||||
}
|
}
|
||||||
if line.len() > 0 {
|
if line.len() > 0 {
|
||||||
self.page.push((start_x, y, line, colour));
|
self.page.push((start_x, y, line, colour));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let OutputType::Indent(space) = o {
|
||||||
|
indent = space;
|
||||||
|
if x == 2 {
|
||||||
|
x += indent;
|
||||||
|
}
|
||||||
|
}
|
||||||
if o == OutputType::Newline {
|
if o == OutputType::Newline {
|
||||||
x = 2;
|
x = 2;
|
||||||
y += LINE_HEIGHT;
|
y += LINE_HEIGHT;
|
||||||
|
line_count += 1;
|
||||||
} else if o == OutputType::EndLink {
|
} else if o == OutputType::EndLink {
|
||||||
colour = false;
|
colour = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.max_lines = (y - 2) / LINE_HEIGHT;
|
self.max_lines = line_count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
42
src/xml.rs
@@ -10,20 +10,38 @@ use ming_wm_lib::utils::Substring;
|
|||||||
//<meta> is bad, <meta/> is good!!
|
//<meta> is bad, <meta/> is good!!
|
||||||
const SELF_CLOSING: [&'static str; 9] = ["link", "meta", "input", "img", "br", "hr", "source", "track", "!DOCTYPE"];
|
const SELF_CLOSING: [&'static str; 9] = ["link", "meta", "input", "img", "br", "hr", "source", "track", "!DOCTYPE"];
|
||||||
|
|
||||||
|
//not all of them, eg there is intentionally no div
|
||||||
|
const BLOCK_LEVEL: [&'static str; 13] = ["p", "br", "li", "tr", "header", "footer", "section", "h1", "h2", "h3", "h4", "h5", "h6"];
|
||||||
|
|
||||||
|
const REPLACE: [(&'static str, &'static str); 6] = [
|
||||||
|
(" ", " "),
|
||||||
|
("'", "'"),
|
||||||
|
(""", "\""),
|
||||||
|
("/", "/"),
|
||||||
|
(">", ">"),
|
||||||
|
("<", "<"),
|
||||||
|
];
|
||||||
|
|
||||||
fn is_whitespace(c: char) -> bool {
|
fn is_whitespace(c: char) -> bool {
|
||||||
c == ' ' || c == '\x09'
|
c == ' ' || c == '\x09'
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_escaped(s: &str) -> String {
|
fn handle_escaped(s: &str) -> String {
|
||||||
s.replace(" ", " ").replace("'", "'").replace(""", "\"").to_string()
|
let mut s = s.to_string();
|
||||||
|
for rp in REPLACE {
|
||||||
|
s = s.replace(rp.0, rp.1);
|
||||||
|
}
|
||||||
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum OutputType {
|
pub enum OutputType {
|
||||||
StartLink(String),
|
StartLink(String),
|
||||||
EndLink,
|
EndLink,
|
||||||
Text(String),
|
Text(String),
|
||||||
Newline,
|
Newline,
|
||||||
|
//only support one per line, once indented, will keep being indented until overriden, for now
|
||||||
|
Indent(usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default, Debug, PartialEq)]
|
#[derive(Clone, Default, Debug, PartialEq)]
|
||||||
@@ -45,14 +63,23 @@ impl Node {
|
|||||||
} else if self.tag_name == "script" || self.tag_name == "style" {
|
} else if self.tag_name == "script" || self.tag_name == "style" {
|
||||||
//ignore script and style tags
|
//ignore script and style tags
|
||||||
return output;
|
return output;
|
||||||
|
} else if self.tag_name == "li" {
|
||||||
|
output.push(OutputType::Text("-".to_string()));
|
||||||
} else if let Some(href) = self.attributes.get("href") {
|
} else if let Some(href) = self.attributes.get("href") {
|
||||||
link = true;
|
link = true;
|
||||||
output.push(OutputType::StartLink(href.to_string()));
|
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);
|
||||||
|
if let Ok(indent) = indent.parse::<usize>() {
|
||||||
|
output.push(OutputType::Indent(indent * 32));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for c in &self.children {
|
for c in &self.children {
|
||||||
output.extend(c.to_output());
|
output.extend(c.to_output());
|
||||||
}
|
}
|
||||||
if self.tag_name == "p" || self.tag_name == "br" || self.tag_name == "li" || self.tag_name == "tr" {
|
if BLOCK_LEVEL.contains(&self.tag_name.as_str()) {
|
||||||
output.push(OutputType::Newline);
|
output.push(OutputType::Newline);
|
||||||
} else if link {
|
} else if link {
|
||||||
output.push(OutputType::EndLink);
|
output.push(OutputType::EndLink);
|
||||||
@@ -274,4 +301,13 @@ fn test_comments_xml_parse() {
|
|||||||
assert!(nodes[1].children[0].tag_name == " afterwards");
|
assert!(nodes[1].children[0].tag_name == " afterwards");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*#[test]
|
||||||
|
fn test_real() {
|
||||||
|
use std::fs::read_to_string;
|
||||||
|
let nodes = parse(&read_to_string("./real_tests/wikipedia.html").unwrap());
|
||||||
|
println!("{:#?}", nodes[1].children);
|
||||||
|
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
|
//more tests 100% needed. yoink from news.ycombinator.com and en.wikipedia.org
|
||||||
|
|||||||