Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
ca9d4cfaf9 | |||
1321e0dcaf | |||
a7468d3f40 |
18
README.md
18
README.md
@ -1,20 +1,24 @@
|
||||
# Markdown web browser
|
||||
A web browser that let's you browse 'mttp' websites that use markdown as a superior standard to html
|
||||
Fully static!
|
||||
A web browser that let's you browse gemini capsules that use gemtext as a superior standard to html.
|
||||
|
||||
# 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.
|
||||
# Requirements
|
||||
- gemget
|
||||
|
||||
# Installing
|
||||
## Build from source:
|
||||
- Clone the repo
|
||||
```cargo run -r```
|
||||
|
||||
# Help
|
||||
Type h in the program to see this text:
|
||||
```
|
||||
Source code: https://git.javalsai.dynv6.net/deadvey/markdown-webbrowser
|
||||
q: quit
|
||||
d: debug info
|
||||
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
|
||||
i: visit root index of this host eg: root index of gemini://deadvey.com/blog/4.md is just gemini://deadvey.com
|
||||
b: go back in history
|
||||
f: go forward in history
|
||||
ox: print the hyprlink of reference x eg: o5 or o24
|
||||
@ -25,6 +29,8 @@ ox: print the hyprlink of reference x eg: o5 or o24
|
||||

|
||||
|
||||
# TO DO
|
||||
- Make pages scrollable
|
||||
- Bookmarks
|
||||
- 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.
|
||||
|
195
src/main.rs
195
src/main.rs
@ -1,188 +1,71 @@
|
||||
use std::process::{Command};
|
||||
use std::io::{stdin,stdout,Write};
|
||||
use colored::Colorize;
|
||||
use regex::Regex;
|
||||
use url::{Url, ParseError};
|
||||
use std::fs;
|
||||
|
||||
const DEBUG_MODE: bool = false;
|
||||
|
||||
// Import other files
|
||||
mod parse_gemtext;
|
||||
|
||||
fn clear_screen() {
|
||||
if DEBUG_MODE == false
|
||||
{
|
||||
println!("clearing");
|
||||
Command::new("clear")
|
||||
.status()
|
||||
.expect("Failed to clear screen");
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_markdown(page_content: String) -> (String, Vec<String>) {
|
||||
let mut parsed_page_content: String = "".to_string();
|
||||
let mut hyperlink_number_counter: u64 = 0;
|
||||
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
|
||||
|
||||
for line in page_content.lines() {
|
||||
let mut parsed_line: String = line.to_string();
|
||||
// Bold
|
||||
let bold_regex = Regex::new(r"((\*\*)|(__))(.*?)((\*\*)|(__))").unwrap();
|
||||
parsed_line = bold_regex.replace_all(&parsed_line, |caps: ®ex::Captures| {
|
||||
caps[4].bold().to_string()
|
||||
}).to_string();
|
||||
|
||||
// Strikethrough
|
||||
let strikethrough_regex = Regex::new(r"~~(.*?)~~").unwrap();
|
||||
parsed_line = strikethrough_regex.replace_all(&parsed_line, |caps: ®ex::Captures| {
|
||||
caps[1].strikethrough().to_string()
|
||||
}).to_string();
|
||||
|
||||
// Horizontal lines
|
||||
let hr_regex = Regex::new(r"^(\*\*\*)|(---)|(___)$").unwrap();
|
||||
parsed_line = hr_regex.replace_all(&parsed_line, |_caps: ®ex::Captures| {
|
||||
let mut result: String = "\n".to_string();
|
||||
for _x in 0..screen_width/2 {
|
||||
result += "- ";
|
||||
}
|
||||
result += "\n";
|
||||
result
|
||||
}).to_string();
|
||||
|
||||
// html br tag support
|
||||
let br_regex = Regex::new(r"(.*?)<br/>(.*?)").unwrap();
|
||||
parsed_line = br_regex.replace_all(&parsed_line, |caps: ®ex::Captures| {
|
||||
format!("{}{}{}", &caps[1], "\n", &caps[2])
|
||||
}).to_string();
|
||||
|
||||
// Italics
|
||||
let italic_regex = Regex::new(r"\*(.*?)\*").unwrap();
|
||||
parsed_line = italic_regex.replace_all(&parsed_line, |caps: ®ex::Captures| {
|
||||
caps[1].italic().to_string()
|
||||
}).to_string();
|
||||
let italic_regex = Regex::new(r"_(.*?)_").unwrap();
|
||||
parsed_line = italic_regex.replace_all(&parsed_line, |caps: ®ex::Captures| {
|
||||
caps[1].italic().to_string()
|
||||
}).to_string();
|
||||
|
||||
// Block quotes
|
||||
let block_quotes_regex = Regex::new(r"^>(.*)").unwrap();
|
||||
parsed_line = block_quotes_regex.replace_all(&parsed_line, |caps: ®ex::Captures| {
|
||||
format!(" | {}", &caps[1])
|
||||
}).to_string();
|
||||
|
||||
// Ordered list
|
||||
let ordered_list_regex = Regex::new(r"^([ \t]+|^)([0-9]+)\. (.*)").unwrap();
|
||||
parsed_line = ordered_list_regex.replace_all(&parsed_line, |caps: ®ex::Captures| {
|
||||
format!("{} {}. {}", &caps[1], &caps[2], &caps[3])
|
||||
}).to_string();
|
||||
|
||||
// Unordered list ([ ]+|^)- (.*)
|
||||
let unordered_list_regex = Regex::new(r"^([ \t]+|^)(-|\+|\*).(.*)").unwrap();
|
||||
parsed_line = unordered_list_regex.replace_all(&parsed_line, |caps: ®ex::Captures| {
|
||||
format!("{} • {}", &caps[1], &caps[3])
|
||||
}).to_string();
|
||||
|
||||
// Inline code
|
||||
let inline_code_regex = Regex::new(r"`([^`]+?)`").unwrap();
|
||||
parsed_line = inline_code_regex.replace_all(&parsed_line, |caps: ®ex::Captures| {
|
||||
format!("{}", &caps[1].magenta())
|
||||
}).to_string();
|
||||
|
||||
// HyperLink
|
||||
let hyperlink_regex = Regex::new(r"(.*?)\[(.*?)\]\((.*?)\)").unwrap();
|
||||
parsed_line = hyperlink_regex.replace_all(&parsed_line, |caps: ®ex::Captures| {
|
||||
// Check if the character before the link is not '!'
|
||||
if !caps[1].ends_with('!') { // caps[1] is everything before the link
|
||||
let result = format!("{}{}[{}]", &caps[1], &caps[2].blue().underline(), hyperlink_number_counter);
|
||||
let url = caps[3].to_string();
|
||||
links.push(url);
|
||||
hyperlink_number_counter += 1;
|
||||
result
|
||||
} else {
|
||||
// If it's an image (starts with !), return the link as is
|
||||
let url = caps[3].to_string();
|
||||
links.push(url);
|
||||
hyperlink_number_counter += 1;
|
||||
format!("({})[{}]", &caps[2].green(), hyperlink_number_counter)
|
||||
}
|
||||
}).to_string();
|
||||
|
||||
let quick_hyperlink_regex = Regex::new(r"<(.*:\/\/.*)>").unwrap();
|
||||
parsed_line = quick_hyperlink_regex.replace_all(&parsed_line, |caps: ®ex::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");
|
||||
}
|
||||
|
||||
// multiline code
|
||||
let multiline_code_regex = Regex::new(r"(?ms)```((.*?\n)+?)```").unwrap();
|
||||
parsed_page_content = multiline_code_regex.replace_all(&parsed_page_content, |caps: ®ex::Captures| {
|
||||
// Capture the code inside the %% blocks
|
||||
let code_block = &caps[1];
|
||||
|
||||
// Add a tab to each line in the block
|
||||
let indented_code = code_block
|
||||
.lines()
|
||||
.map(|line| format!("\t{}", line)) // Insert tab at the start of each line
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
|
||||
// Return the formatted block with magenta color
|
||||
format!("{}", indented_code.magenta())
|
||||
}).to_string();
|
||||
|
||||
return (parsed_page_content, links);
|
||||
}
|
||||
|
||||
fn fetch_page(url: &Url) -> String {
|
||||
fn fetch_page(url: &Url) {
|
||||
let full_url_formatted = format!("{}", url);
|
||||
|
||||
// Call curl using Com, mand
|
||||
let output = Command::new("curl")
|
||||
.arg(full_url_formatted)
|
||||
let output = Command::new("gemget")
|
||||
.args([full_url_formatted, "-o".to_string(), "/tmp/page".to_string()])
|
||||
.output()
|
||||
.expect("Failed to execute curl command");
|
||||
.expect("Failed to execute gemget command");
|
||||
|
||||
// Check if the command was successful
|
||||
if output.status.success() {
|
||||
let page: String = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
return page
|
||||
} else {
|
||||
eprintln!("Error:\n{}", String::from_utf8_lossy(&output.stderr));
|
||||
let result: String = "error".to_string();
|
||||
return result
|
||||
if ! output.status.success() {
|
||||
println!("{}\n{:?}\n", "Failed to fetch page:".red(), output);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_page(url: Url, source: bool) -> Vec<String> {
|
||||
clear_screen();
|
||||
let mut content = fetch_page(&url);
|
||||
fetch_page(&url);
|
||||
let mut links = Vec::new();
|
||||
if let Ok(mut content) = fs::read_to_string::<String>("/tmp/page".to_string()) {
|
||||
Command::new("rm")
|
||||
.arg("/tmp/page")
|
||||
.output()
|
||||
.expect("Failed to delete tmp page");
|
||||
let (screen_width, _screen_height) = termion::terminal_size().unwrap();
|
||||
|
||||
if source == true {
|
||||
content += &format!("{}", &"Viewing source code".yellow());
|
||||
}
|
||||
else if &content[..13] == "<!DOCTYPE md>" {
|
||||
(content, links) = parse_markdown((&content[13..]).to_string());
|
||||
}
|
||||
else {
|
||||
content += &format!("{}", &"Warning: This page is invalid markdown, it should contain <!DOCTYPE md> at the very start of the file, showing raw text".yellow());
|
||||
(content, links) = parse_gemtext::parse_gemtext(content);
|
||||
}
|
||||
|
||||
for _i in 0..screen_width {
|
||||
print!("—");
|
||||
print!("-");
|
||||
}
|
||||
print!("{}\n", url);
|
||||
for _i in 0..screen_width {
|
||||
print!("—");
|
||||
print!("-");
|
||||
}
|
||||
println!("\n\n{}", content);
|
||||
for _i in 0..screen_width {
|
||||
print!("—");
|
||||
print!("-");
|
||||
}
|
||||
println!();
|
||||
|
||||
// Return links (you can add link parsing logic)
|
||||
}
|
||||
return links;
|
||||
}
|
||||
|
||||
@ -206,18 +89,14 @@ fn parse_url(user_input: String, previous_url: &Url) -> Result<Url, ParseError>
|
||||
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
|
||||
format!("gemini://{}/{}",Url::host_str(previous_url).expect("ivalid").to_string(), user_input)
|
||||
};
|
||||
|
||||
println!("Parsing: {}", to_parse);
|
||||
if let Ok(mut url) = Url::parse(&to_parse) {
|
||||
if url.port() == None {
|
||||
let _ = url.set_port(Some(3477));
|
||||
let _ = url.set_port(Some(1965));
|
||||
}
|
||||
println!("{:?}",url);
|
||||
println!("{}",url.as_str());
|
||||
@ -235,7 +114,7 @@ fn main() {
|
||||
println!("Enter a url: ");
|
||||
let user_input = input();
|
||||
|
||||
if user_input == "q" {
|
||||
if user_input == "q" || user_input == "quit" || user_input == "exit" {
|
||||
std::process::exit(0);
|
||||
}
|
||||
let mut load_page: bool = true;
|
||||
@ -243,19 +122,30 @@ fn main() {
|
||||
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 ;)
|
||||
if let Ok(mut url) = parse_url(user_input, &Url::parse(&"gemini://geminiprotocol.net").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");
|
||||
}
|
||||
url = history[historical_position].clone();
|
||||
load_page = false;
|
||||
|
||||
let user_input = input();
|
||||
if user_input == "q" {
|
||||
break 'mainloop;
|
||||
}
|
||||
else if user_input == "d" {
|
||||
println!(
|
||||
"load_page: {}\nhistory: {:?}\nhistorical_postition: {}\nlinks: {:?}\nsource: {}",
|
||||
load_page,
|
||||
history,
|
||||
historical_position,
|
||||
links,
|
||||
source
|
||||
);
|
||||
}
|
||||
else if user_input == "r" {
|
||||
load_page = true;
|
||||
continue;
|
||||
@ -294,10 +184,11 @@ fn main() {
|
||||
else if user_input == "h" {
|
||||
println!("Source code: https://git.javalsai.dynv6.net/deadvey/markdown-webbrowser
|
||||
q: quit
|
||||
d: debug info
|
||||
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
|
||||
i: visit root index of this host eg: root index of gemini://deadvey.com/blog/4.md is just gemini://deadvey.com
|
||||
b: go back in history
|
||||
f: go forward in history
|
||||
ox: print the hyprlink of reference x eg: o5 or o24
|
||||
|
91
src/parse_gemtext.rs
Normal file
91
src/parse_gemtext.rs
Normal file
@ -0,0 +1,91 @@
|
||||
use colored::Colorize;
|
||||
use regex::Regex;
|
||||
|
||||
pub fn parse_gemtext(page_content: String) -> (String, Vec<String>) {
|
||||
let mut parsed_page_content: String = "".to_string();
|
||||
let mut hyperlink_number_counter: u64 = 0;
|
||||
let mut links: Vec<String> = Vec::new();
|
||||
let mut preformatted_code_toggle = false;
|
||||
|
||||
// Regex patterns
|
||||
let preformatted_text_regex = Regex::new(r"^```(.*)").unwrap();
|
||||
let header1_regex = Regex::new(r"^# (.*)").unwrap();
|
||||
let header2_regex = Regex::new(r"^## (.*)").unwrap();
|
||||
let header3_regex = Regex::new(r"^### (.*)").unwrap();
|
||||
let block_quotes_regex = Regex::new(r"^>(.*)").unwrap();
|
||||
let unordered_list_regex = Regex::new(r"^([ \t]+|^)(\*).(.*)").unwrap();
|
||||
let hyperlink_regex = Regex::new(r"=>\s(\S*)\s(.*)").unwrap();
|
||||
let quick_hyperlink_regex = Regex::new(r"=>\s(.*)").unwrap();
|
||||
|
||||
for line in page_content.lines() {
|
||||
let mut parsed_line: String = line.to_string();
|
||||
let mut remove_line = false;
|
||||
|
||||
// preformatted text
|
||||
parsed_line = preformatted_text_regex.replace_all(&parsed_line, |caps: ®ex::Captures| {
|
||||
// Flip the toggle
|
||||
preformatted_code_toggle = ! preformatted_code_toggle;
|
||||
|
||||
if caps[1] == *""
|
||||
{
|
||||
remove_line = true;
|
||||
}
|
||||
|
||||
// Remove the ```
|
||||
format!("{}", &caps[1].magenta())
|
||||
}).to_string();
|
||||
|
||||
if preformatted_code_toggle == false
|
||||
{
|
||||
// Headers
|
||||
parsed_line = header1_regex.replace_all(&parsed_line, |caps: ®ex::Captures| {
|
||||
format!("{}", &caps[1].blue().bold().underline())
|
||||
}).to_string();
|
||||
parsed_line = header2_regex.replace_all(&parsed_line, |caps: ®ex::Captures| {
|
||||
format!("{}", &caps[1].blue().bold())
|
||||
}).to_string();
|
||||
parsed_line = header3_regex.replace_all(&parsed_line, |caps: ®ex::Captures| {
|
||||
format!("{}", &caps[1].bold())
|
||||
}).to_string();
|
||||
|
||||
// Block quotes
|
||||
parsed_line = block_quotes_regex.replace_all(&parsed_line, |caps: ®ex::Captures| {
|
||||
format!(" | {}", &caps[1].red())
|
||||
}).to_string();
|
||||
|
||||
// Unordered list ([ ]+|^)- (.*)
|
||||
parsed_line = unordered_list_regex.replace_all(&parsed_line, |caps: ®ex::Captures| {
|
||||
format!("{} • {}", &caps[1], &caps[3])
|
||||
}).to_string();
|
||||
|
||||
// HyperLink
|
||||
parsed_line = hyperlink_regex.replace_all(&parsed_line, |caps: ®ex::Captures| {
|
||||
// Check if the character before the link is not '!'
|
||||
let result = format!("[{}] {}", hyperlink_number_counter, &caps[2].blue().underline());
|
||||
let url = caps[1].to_string();
|
||||
links.push(url);
|
||||
hyperlink_number_counter += 1;
|
||||
result
|
||||
}).to_string();
|
||||
|
||||
parsed_line = quick_hyperlink_regex.replace_all(&parsed_line, |caps: ®ex::Captures| {
|
||||
hyperlink_number_counter += 1;
|
||||
let url = caps[1].to_string();
|
||||
links.push(url);
|
||||
format!("[{}] {}", hyperlink_number_counter, &caps[1].blue().underline())
|
||||
}).to_string();
|
||||
}
|
||||
else if preformatted_code_toggle == true
|
||||
{
|
||||
parsed_line = parsed_line.magenta().to_string();
|
||||
}
|
||||
|
||||
if remove_line == false
|
||||
{
|
||||
parsed_page_content+=&(parsed_line + "\n");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (parsed_page_content, links);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user