Compare commits

..

21 Commits

Author SHA1 Message Date
845866ef9d readme 2025-01-26 11:15:34 +00:00
5f856f35fe source code viewing 2025-01-26 02:59:06 +00:00
7b9d2d6fd3 some stuff 2025-01-26 02:39:43 +00:00
f46d3bac42 fixed history functionality, TO DO: arguments, documentation 2025-01-26 02:12:56 +00:00
ced5648c01 url parsing works (FINALLY) 2025-01-26 01:44:24 +00:00
7b07b6f051 done for the day, got a proper url parser 2025-01-22 01:18:30 +00:00
e4b08b45bc ``` 2025-01-21 23:59:18 +00:00
1b8614b956 open http links in web browser now :D 2025-01-21 19:04:40 +00:00
d55d70ec45 <!DOCTYPE md> now required 2025-01-21 18:33:14 +00:00
67a5096267 going back in history sort of works, but is quite buggy 2025-01-13 14:43:00 +00:00
0da9e95b2a sorta done history and going back, but gonna go sleep now, not finished btw 2025-01-13 00:52:58 +00:00
2aa4a82af9 made input's more safe 2025-01-12 23:20:30 +00:00
deadvey
88200eb354 http stuff in regex 2025-01-12 02:29:11 +00:00
deadvey
bb75241a3c ignore swap files ¬_¬! 2025-01-12 02:20:16 +00:00
deadvey
4e7422c478 made code a bit safer, harder to crash 2025-01-12 02:19:05 +00:00
deadvey
3d12793550 added a todo 2025-01-09 01:07:28 +00:00
deadvey
859ec9fcfc error yaps 2025-01-09 00:59:30 +00:00
deadvey
a1dfc6bd04 readme 2025-01-09 00:55:07 +00:00
deadvey
b72d43b250 Sublists work now 2025-01-09 00:47:29 +00:00
deadvey
8e860b89e7 quick links and <br/> support 2025-01-08 23:42:01 +00:00
deadvey
590cd001be few rendering changes 2025-01-08 20:35:59 +00:00
6 changed files with 263 additions and 111 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
Cargo.lock
target
*.swp

View File

@@ -6,5 +6,6 @@ edition = "2021"
[dependencies] [dependencies]
colored = "2.2.0" colored = "2.2.0"
regex = "1.11.1" regex = "1.11.1"
url = "2.5.4"
termion = "4.0.3" termion = "4.0.3"
open = "5.3.2"
url = "2.5.4"

42
README.md Normal file
View File

@@ -0,0 +1,42 @@
# Markdown web browser
A web browser that let's you browse 'mttp' websites that use markdown as a superior standard to html
Fully static!
# Getting a website on this
The default port is 3477, though you can use any port as long as you specify it in the url.
You need a <!DOCTYPE md> tag at the start of any markdown files so the browser know's which files are markdown and which are other generic text files.
# Help
Type h in the program to see this text:
```
Source code: https://git.javalsai.dynv6.net/deadvey/markdown-webbrowser
q: quit
h: help
r: reload
s: view source code of page
i: visit root index of this host eg: root index of mttp://deadvey.com/blog/4.md is just deadvey.com
b: go back in history
f: go forward in history
ox: print the hyprlink of reference x eg: o5 or o24
[url]: follow the inputed url
```
# Example:
![screenshot](/images/screenshot.png)
# TO DO
- Properly comment it because I can't lie I can't even follow it.
- Make it memory safe, it crashes if the input is unexpected.
- Use treesitter instead of Regex, because, reasons.
- "wtf deadvey" - [error](https://git.javalsai.dynv6.net/ErrorNoInternet)
- "Don't use Regex to parse **ANYTHING**" - [error](https://git.javalsai.dynv6.net/ErrorNoInternet)
- "use treesitter" - [error](https://git.javalsai.dynv6.net/ErrorNoInternet)
- "yeah, definitley use treesitter" - [error](https://git.javalsai.dynv6.net/ErrorNoInternet)
- "use treesitter" - [error](https://git.javalsai.dynv6.net/ErrorNoInternet)
- "or glow" - [error](https://git.javalsai.dynv6.net/ErrorNoInternet)
- "I found another markdown to terminal converter" - [error](https://git.javalsai.dynv6.net/ErrorNoInternet)
- "ban [for using regex]" - [error](https://git.javalsai.dynv6.net/ErrorNoInternet)
- "use treesitter" - [error](https://git.javalsai.dynv6.net/ErrorNoInternet)
- Get a catchier name, 'markdown web browser' sounds kind of lame.
![error yapping](/images/error_yapping.gif)

BIN
images/error_yapping.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

BIN
images/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 880 KiB

View File

@@ -2,30 +2,33 @@ use std::process::{Command};
use std::io::{stdin,stdout,Write}; use std::io::{stdin,stdout,Write};
use colored::Colorize; use colored::Colorize;
use regex::Regex; use regex::Regex;
use url::{Url, ParseError};
fn clear_screen() { fn clear_screen() {
println!("clearing");
Command::new("clear") Command::new("clear")
.status() .status()
.expect("Failed to clear screen"); .expect("Failed to clear screen");
//println!("clearing");
} }
fn parse_markdown(page_content: String) -> (String, Vec<String>) { fn parse_markdown(page_content: String) -> (String, Vec<String>) {
let mut parsed_page_content: String = "".to_string(); let mut parsed_page_content: String = "".to_string();
let mut hyperlink_number_counter: u64 = 0; let mut hyperlink_number_counter: u64 = 0;
let mut links: Vec<String> = Vec::new(); let mut links: Vec<String> = Vec::new();
let (screen_width, screen_height) = termion::terminal_size().unwrap(); // So the horizontal line (<hr/>) spans the whole console let (screen_width, _screen_height) = termion::terminal_size().unwrap(); // So the horizontal line (<hr/>) spans the whole console
for line in page_content.lines() { for line in page_content.lines() {
let mut parsed_line: String = line.to_string(); let mut parsed_line: String = line.to_string();
// Bold // Bold
let bold_regex = Regex::new(r"\*\*(.*?)\*\*").unwrap(); let bold_regex = Regex::new(r"((\*\*)|(__))(.*?)((\*\*)|(__))").unwrap();
parsed_line = bold_regex.replace_all(&parsed_line, |caps: &regex::Captures| { parsed_line = bold_regex.replace_all(&parsed_line, |caps: &regex::Captures| {
caps[1].bold().to_string() caps[4].bold().to_string()
}).to_string(); }).to_string();
let bold_regex = Regex::new(r"__(.*?)__").unwrap();
parsed_line = bold_regex.replace_all(&parsed_line, |caps: &regex::Captures| { // Strikethrough
caps[1].bold().to_string() let strikethrough_regex = Regex::new(r"~~(.*?)~~").unwrap();
parsed_line = strikethrough_regex.replace_all(&parsed_line, |caps: &regex::Captures| {
caps[1].strikethrough().to_string()
}).to_string(); }).to_string();
// Horizontal lines // Horizontal lines
@@ -39,6 +42,12 @@ fn parse_markdown(page_content: String) -> (String, Vec<String>) {
result result
}).to_string(); }).to_string();
// html br tag support
let br_regex = Regex::new(r"(.*?)<br/>(.*?)").unwrap();
parsed_line = br_regex.replace_all(&parsed_line, |caps: &regex::Captures| {
format!("{}{}{}", &caps[1], "\n", &caps[2])
}).to_string();
// Italics // Italics
let italic_regex = Regex::new(r"\*(.*?)\*").unwrap(); let italic_regex = Regex::new(r"\*(.*?)\*").unwrap();
parsed_line = italic_regex.replace_all(&parsed_line, |caps: &regex::Captures| { parsed_line = italic_regex.replace_all(&parsed_line, |caps: &regex::Captures| {
@@ -52,19 +61,23 @@ fn parse_markdown(page_content: String) -> (String, Vec<String>) {
// Block quotes // Block quotes
let block_quotes_regex = Regex::new(r"^>(.*)").unwrap(); let block_quotes_regex = Regex::new(r"^>(.*)").unwrap();
parsed_line = block_quotes_regex.replace_all(&parsed_line, |caps: &regex::Captures| { parsed_line = block_quotes_regex.replace_all(&parsed_line, |caps: &regex::Captures| {
format!(" | {}", &caps[1].on_black()) format!(" | {}", &caps[1])
}).to_string(); }).to_string();
// Ordered list // Ordered list
let ordered_list_regex = Regex::new(r"^([0-9]+)\.(.*)").unwrap(); let ordered_list_regex = Regex::new(r"^([ \t]+|^)([0-9]+)\. (.*)").unwrap();
parsed_line = ordered_list_regex.replace_all(&parsed_line, " $1. $2").to_string(); parsed_line = ordered_list_regex.replace_all(&parsed_line, |caps: &regex::Captures| {
format!("{} {}. {}", &caps[1], &caps[2], &caps[3])
}).to_string();
// Unordered list // Unordered list ([ ]+|^)- (.*)
let unordered_list_regex = Regex::new(r"^(-|\+|\*).(.*)").unwrap(); let unordered_list_regex = Regex::new(r"^([ \t]+|^)(-|\+|\*).(.*)").unwrap();
parsed_line = unordered_list_regex.replace_all(&parsed_line, " • $2").to_string(); parsed_line = unordered_list_regex.replace_all(&parsed_line, |caps: &regex::Captures| {
format!("{}{}", &caps[1], &caps[3])
}).to_string();
// Inline code // Inline code
let inline_code_regex = Regex::new(r"`(.*)`").unwrap(); let inline_code_regex = Regex::new(r"`([^`]+?)`").unwrap();
parsed_line = inline_code_regex.replace_all(&parsed_line, |caps: &regex::Captures| { parsed_line = inline_code_regex.replace_all(&parsed_line, |caps: &regex::Captures| {
format!("{}", &caps[1].magenta()) format!("{}", &caps[1].magenta())
}).to_string(); }).to_string();
@@ -81,16 +94,27 @@ fn parse_markdown(page_content: String) -> (String, Vec<String>) {
result result
} else { } else {
// If it's an image (starts with !), return the link as is // If it's an image (starts with !), return the link as is
format!("[{}]", &caps[2].green()) let url = caps[3].to_string();
links.push(url);
hyperlink_number_counter += 1;
format!("({})[{}]", &caps[2].green(), hyperlink_number_counter)
} }
}).to_string(); }).to_string();
let quick_hyperlink_regex = Regex::new(r"<(.*:\/\/.*)>").unwrap();
parsed_line = quick_hyperlink_regex.replace_all(&parsed_line, |caps: &regex::Captures| {
hyperlink_number_counter += 1;
let url = caps[1].to_string();
links.push(url);
format!("{}[{}]", &caps[1].blue().underline(), hyperlink_number_counter)
}).to_string();
parsed_page_content+=&(parsed_line + "\n"); parsed_page_content+=&(parsed_line + "\n");
} }
// multiline code // multiline code
let multiline_code_regex = Regex::new(r"(?ms)%%%((.*?\n)+?)%%%").unwrap(); let multiline_code_regex = Regex::new(r"(?ms)```((.*?\n)+?)```").unwrap();
parsed_page_content = multiline_code_regex.replace_all(&parsed_page_content, |caps: &regex::Captures| { parsed_page_content = multiline_code_regex.replace_all(&parsed_page_content, |caps: &regex::Captures| {
// Capture the code inside the %% blocks // Capture the code inside the %% blocks
let code_block = &caps[1]; let code_block = &caps[1];
@@ -109,8 +133,8 @@ fn parse_markdown(page_content: String) -> (String, Vec<String>) {
return (parsed_page_content, links); return (parsed_page_content, links);
} }
fn fetch_page(host: &String, port: &String, path: &String) -> String { fn fetch_page(url: &Url) -> String {
let full_url_formatted = format!("{}:{}/{}", host, port, path); let full_url_formatted = format!("{}", url);
// Call curl using Com, mand // Call curl using Com, mand
let output = Command::new("curl") let output = Command::new("curl")
@@ -129,23 +153,37 @@ fn fetch_page(host: &String, port: &String, path: &String) -> String {
} }
} }
fn render_page(host: String, port: String, path: String) -> Vec<String> { fn render_page(url: Url, source: bool) -> Vec<String> {
clear_screen(); clear_screen();
let mut content = fetch_page(&host, &port, &path); let mut content = fetch_page(&url);
let mut links = Vec::new(); let mut links = Vec::new();
let (screen_width, screen_height) = termion::terminal_size().unwrap(); let (screen_width, _screen_height) = termion::terminal_size().unwrap();
(content, links) = parse_markdown(content); if source == true {
print!("{}: {}\n", host, path); content += &format!("{}", &"Viewing source code".yellow());
for _i in 0..screen_width { }
print!(""); else if &content[..13] == "<!DOCTYPE md>" {
} (content, links) = parse_markdown((&content[13..]).to_string());
println!("\n\n{}", content); }
for _i in 0..screen_width { else {
print!(""); content += &format!("{}", &"Warning: This page is invalid markdown, it should contain <!DOCTYPE md> at the very start of the file, showing raw text".yellow());
} }
println!();
return links; for _i in 0..screen_width {
print!("");
}
print!("{}\n", url);
for _i in 0..screen_width {
print!("");
}
println!("\n\n{}", content);
for _i in 0..screen_width {
print!("");
}
println!();
// Return links (you can add link parsing logic)
return links;
} }
fn input() -> String{ fn input() -> String{
@@ -161,84 +199,152 @@ fn input() -> String{
return s; return s;
} }
fn parse_url(url: String, previous_host: &String) -> (String, String, String) { fn parse_url(user_input: String, previous_url: &Url) -> Result<Url, ParseError> {
let mut host: String = previous_host.to_string(); println!("user input: {}",user_input);
let mut port: String = "3477".to_string(); println!("previous url: {:?}",previous_url);
let mut path: String = "/".to_string(); let to_parse = if user_input.contains("://") {
println!("Contains different scheme or is a path");
user_input
}
else if user_input[..1] == *"/" {
format!("http://{}/{}",Url::host_str(previous_url).expect("ivalid").to_string(), user_input)
}
else {
println!("prepending scheme to user input");
format!("http://{}", user_input) // Prepend 'mttp://' if no scheme is found
};
let mttp_regex = Regex::new(r"^mttp:\/\/(.*?)\/(.*?)$").unwrap(); println!("Parsing: {}", to_parse);
let mttp_regex_no_path = Regex::new(r"^mttp:\/\/(.*?)$").unwrap(); if let Ok(mut url) = Url::parse(&to_parse) {
let accept_this_as_mttp = Regex::new(r"^(.*?)$").unwrap(); if url.port() == None {
let path_change = Regex::new(r"^/(.*?)$").unwrap(); let _ = url.set_port(Some(3477));
}
println!("{:?}",url);
println!("{}",url.as_str());
println!("parsed successfully");
return Ok(url)
}
else {
return Err(ParseError::InvalidDomainCharacter)
}
if let Some(caps) = mttp_regex.captures(&url) {
host = caps[1].to_string();
port = "3477".to_string();
path = caps[2].to_string();
}
else if let Some(caps) = mttp_regex_no_path.captures(&url) {
host = caps[1].to_string();
port = "3477".to_string();
path = "/".to_string();
}
else if let Some(caps) = path_change.captures(&url) {
println!("path change");
host = previous_host.to_string();
port = "3477".to_string();
path = caps[1].to_string();
}
else if let Some(caps) = accept_this_as_mttp.captures(&url) {
host = caps[1].to_string();
port = "3477".to_string();
path = "/".to_string();
}
println!("{}:{}/{}",host,port,path);
return (host, port, path);
} }
fn main() { fn main() {
clear_screen(); clear_screen();
println!("Enter a url: "); println!("Enter a url: ");
let url = input(); let user_input = input();
if url == "q" { if user_input == "q" {
std::process::exit(0); std::process::exit(0);
} }
let mut load_page: bool = true;
let mut history: Vec<Url> = Vec::new();
let mut historical_position: usize = 0;
let mut links: Vec<String> = Vec::new();
let mut source: bool = false; // Wether to view source of markdown page or rendered version
if let Ok(mut url) = parse_url(user_input, &Url::parse(&"http://deadvey.com").unwrap()) { // Change this and make internal pages ;)
history.push(url.clone());
'mainloop: loop {
if load_page {
links = render_page(history[historical_position].clone(), source);
println!("Enter reference number to follow, h for help, or q to quit");
}
load_page = false;
let (mut host, mut port, mut path) = parse_url(url, &"example.com".to_string()); // Must pass let user_input = input();
// 'previous if user_input == "q" {
// host' break 'mainloop;
}
'mainloop: loop { else if user_input == "r" {
let links = render_page(host.clone(), port.clone(), path.clone()); load_page = true;
continue;
println!("{}:{}/{}", host, port, path); }
else if user_input == "s" {
println!("Enter reference number to follow, h for help, or q to quit"); source = ! source; // Flip the boolean to toggle source mode
let link_to_follow = input(); load_page = true;
if link_to_follow == "q" { }
break 'mainloop; else if user_input == "i" {
} let _ = url.set_path("/");
else if link_to_follow == "r" { for _i in historical_position+1..history.len() {
continue; history.remove(historical_position+1);
} }
else if link_to_follow == "h" { history.push(url.clone());
println!(" historical_position += 1;
Source code: https://git.javalsai.dynv6.net/deadvey/markdown-webbrowser\n load_page = true;
q: quit }
h: help else if user_input == "b" {
r: reload if historical_position > 0 {
i: visit root index of this host eg: root index of mttp://deadvey.com/blog/4.md is just deadvey.com historical_position -= 1;
b: go back in history load_page = true;
"); }
} else {
else { println!("At start of history");
let number: usize = link_to_follow.parse::<usize>().unwrap(); }
}
(host, port, path) = parse_url(links[number].clone(), &host); else if user_input == "f" {
println!("{}:{}/{}", host, port, path); if historical_position < history.len()-1 {
} historical_position += 1;
} load_page = true;
}
else {
println!("At end of history");
}
}
else if user_input == "h" {
println!("Source code: https://git.javalsai.dynv6.net/deadvey/markdown-webbrowser
q: quit
h: help
r: reload
s: view source code of page
i: visit root index of this host eg: root index of mttp://deadvey.com/blog/4.md is just deadvey.com
b: go back in history
f: go forward in history
ox: print the hyprlink of reference x eg: o5 or o24
[url]: follow the inputed url");
}
else if user_input.chars().nth(0).unwrap() == 'o' {
let number_str = &user_input[1..];
if let Ok(number) = number_str.parse::<usize>() {
println!("{}", links[number]);
} else {
println!("error");
}
}
else if let Ok(number) = user_input.parse::<usize>() {
if number < links.len() {
if let Ok(parsed_value) = parse_url(links[number].clone(), &url.clone()) {
url = parsed_value;
for _i in historical_position+1..history.len() {
history.remove(historical_position+1);
}
history.push(url.clone());
historical_position += 1;
load_page = true;
}
else {
println!("Invalid url\nAttempting to open url in web browser");
}
} else {
println!("Invalid reference id");
}
}
else if let Ok(parsed_value) = parse_url(user_input, &url.clone()) {
url = parsed_value;
for _i in historical_position+1..history.len() {
history.remove(historical_position+1);
}
history.push(url.clone());
historical_position += 1;
load_page = true;
}
else {
println!("Invalid input");
}
}
}
else {
println!("Invalid mttp url, try mttp:// at the start of your input.");
}
} }