gmi2html

library and cli tool to convert gemtext to HTML
git clone git://git.alexwennerberg.com/gmi2html
Log | Files | Refs | README | LICENSE

commit c39dcf7455bc1bb805b320ca23667a30c9b8c7f5
parent b70526fd26ef4a4deb5aa2bdd1f4be8305016f7d
Author: alex wennerberg <alex@alexwennerberg.com>
Date:   Sun, 22 May 2022 17:39:11 -0700

Add CLI

Diffstat:
MCargo.lock | 2+-
Aexamples/gemini_text_guide.gmi | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/lib.rs | 37++++++++++++++-----------------------
Msrc/main.rs | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
4 files changed, 193 insertions(+), 26 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -14,7 +14,7 @@ dependencies = [ [[package]] name = "gmi2html" -version = "0.1.6" +version = "0.1.7" dependencies = [ "url", ] diff --git a/examples/gemini_text_guide.gmi b/examples/gemini_text_guide.gmi @@ -0,0 +1,97 @@ +# Writing Gemtext (.gmi) + +Gemtext is a text format as part of the Gemini project with a very limited set of features. While Flounder converts Gemtext files to HTML so they can be shown in a web browser, Gemtext itself is much simpler than HTML: there are only a handful of tags. To use a tag, you must start the line with that tag. + +### Headers: +``` +# Big +## Smaller +### Smallest +``` +# Big +## Smaller +### Smallest + +### Hyperlinks: +Links can be absolute (https://google.com) or relative (index.gmi). You can link to .gmi files or any file that you upload. For links to content that is hosted at the same URL on Gemini and HTTP(S), such as flounder.online links, I recommend using schemaless URLs such as //alex.flounder.online as they will properly show up on both gemini and http pages. +``` +=> https://google.com +=> index.gmi +=> //flounder.online Link with text +=> car2.jpg +``` +=> https://google.com +=> index.gmi +=> //flounder.online Link with text +=> car2.jpg + +### Lists: +``` +* Item 1 +* Item 2 +* Item 3 +``` +* Item 1 +* Item 2 +* Item 3 + +### Quotes: +``` +>Hello! +``` +>Hello! + +### Raw unformatted text: +``` +​``` +​# raw text +​``` +``` + +``` +# raw text +``` + +For code ascii art, it is recommended that you annotate your text like this for accessibility purposes (e.g. low-vision users): + +``` + ```a text-based heart + _░▒███████ + ░██▓▒░░▒▓██ + ██▓▒░__░▒▓██___██████ + ██▓▒░____░▓███▓__░▒▓██ + ██▓▒░___░▓██▓_____░▒▓██ + ██▓▒░_______________░▒▓██ + _██▓▒░______________░▒▓██ + __██▓▒░____________░▒▓██ + ___██▓▒░__________░▒▓██ + ____██▓▒░________░▒▓██ + _____██▓▒░_____░▒▓██ + ______██▓▒░__░▒▓██ + _______█▓▒░░▒▓██ + _________░▒▓██ + _______░▒▓██ + _____░▒▓██ + ``` +``` + +```a text-based heart +_░▒███████ +░██▓▒░░▒▓██ +██▓▒░__░▒▓██___██████ +██▓▒░____░▓███▓__░▒▓██ +██▓▒░___░▓██▓_____░▒▓██ +██▓▒░_______________░▒▓██ +_██▓▒░______________░▒▓██ +__██▓▒░____________░▒▓██ +___██▓▒░__________░▒▓██ +____██▓▒░________░▒▓██ +_____██▓▒░_____░▒▓██ +______██▓▒░__░▒▓██ +_______█▓▒░░▒▓██ +_________░▒▓██ +_______░▒▓██ +_____░▒▓██ +``` + +(Mouseover the heart in some browsers) diff --git a/src/lib.rs b/src/lib.rs @@ -18,14 +18,11 @@ use std::collections::HashSet; use url::{ParseError, Url}; -static ALLOWED_SCHEMES: &[&str] = &["https", "http", "gemini", "gopher", "mailto"]; - // All 4 characters for efficiency static IMAGE_EXTENSIONS: &[&str] = &[".jpg", "jpeg", ".png", ".gif", ".ico", ".svg", "webp"]; pub struct GeminiConverter<'a> { proxy_url: Option<Url>, - allowed_schemes: HashSet<String>, // TODO allow disallowed configuration input_text: &'a str, inline_images: bool, @@ -36,7 +33,6 @@ impl<'a> GeminiConverter<'a> { pub fn new(gmi_text: &'a str) -> Self { Self { proxy_url: None, - allowed_schemes: ALLOWED_SCHEMES.iter().map(|a| a.to_string()).collect(), // inefficient input_text: gmi_text, inline_images: false, } @@ -55,13 +51,6 @@ impl<'a> GeminiConverter<'a> { self } - /// Applied before proxy_url. - pub fn allowed_schemes(&mut self, allowed: &'a [&'a str]) -> &mut Self { - // Applied before proxy_url - self.allowed_schemes = allowed.iter().map(|a| a.to_string()).collect(); - self - } - /// Convert Gemini text to HTML. pub fn to_html(&self) -> String { // This function sometimes priorities performance over readability @@ -152,22 +141,24 @@ impl<'a> GeminiConverter<'a> { output.push_str("<a href=\""); } if let Ok(p) = parsed { - if self.allowed_schemes.contains(p.scheme()) { - if p.scheme() == "gemini" { - // TODO FIX - if let Some(s) = &self.proxy_url { - // Never fail, just use blank string if cant parse - let join = |a: &Url, b: Url| -> Result<String, Box<dyn std::error::Error>> { - Ok(a.join(b.host_str().ok_or("err")?)?.join(b.path())?.as_str().to_string()) + if p.scheme() == "gemini" { + // TODO FIX + if let Some(s) = &self.proxy_url { + // Never fail, just use blank string if cant parse + let join = + |a: &Url, b: Url| -> Result<String, Box<dyn std::error::Error>> { + Ok(a.join(b.host_str().ok_or("err")?)? + .join(b.path())? + .as_str() + .to_string()) }; - let proxied = join(s, p).unwrap_or("".to_string()); // Dont fail - output.push_str(&proxied); - } else { - output.push_str(p.as_str()); - } + let proxied = join(s, p).unwrap_or("".to_string()); // Dont fail + output.push_str(&proxied); } else { output.push_str(p.as_str()); } + } else { + output.push_str(p.as_str()); } } let link_text = match second.as_str() { diff --git a/src/main.rs b/src/main.rs @@ -1,13 +1,92 @@ use gmi2html::GeminiConverter; +use std::env; +use std::ffi::OsString; use std::io::{self, Read}; +use std::path::PathBuf; +use std::process::exit; +use std::str::FromStr; + fn main() { let mut buffer = String::new(); // Basic CLI with hard-coded defaults TODO add cli + let args = Args::from_env(); io::stdin().read_to_string(&mut buffer).unwrap(); let res = GeminiConverter::new(&buffer) - .proxy_url("https://portal.mozz.us/gemini/") - .inline_images(true) + .proxy_url(&args.proxy_url) + .inline_images(args.inline_images) .to_html(); println!("{}", res); } + +// Arg parser. + +fn usage() -> ! { + let name = env::args().next().unwrap(); + eprintln!( + "usage: {} [-i] [-p PROXY_URL] + +Reads gemtext from stdin, outputs html to stdout + +FLAGS +-i include in-line images (default: false) + +ARGS: +-p proxy URL (default https://portal.mozz.us/gemini/)", + name + ); + exit(1) +} + +#[derive(Default)] +pub struct Args { + pub inline_images: bool, + pub proxy_url: String, +} + +impl Args { + pub fn default() -> Self { + Args { + inline_images: false, + proxy_url: "https://portal.mozz.us/gemini".to_owned(), + } + } + + pub fn from_env() -> Self { + let mut out = Self::default(); + let mut args = env::args_os().skip(1); + while let Some(arg) = args.next() { + let s = arg.to_string_lossy(); + let mut ch_iter = s.chars(); + if ch_iter.next() != Some('-') { + // out.positional.push(arg); + continue; + } + ch_iter.for_each(|m| match m { + // Edit these lines // + 'p' => out.proxy_url = parse_arg(args.next()), + 'i' => out.inline_images = true, + // Stop editing // + _ => { + usage(); + } + }) + } + out + } +} + +#[allow(dead_code)] +fn parse_arg<T: FromStr>(a: Option<OsString>) -> T { + a.and_then(|a| a.into_string().ok()) + .and_then(|a| a.parse().ok()) + .unwrap_or_else(|| usage()) +} + +#[allow(dead_code)] +fn parse_os_arg<T: From<OsString>>(a: Option<OsString>) -> T { + match a { + Some(s) => T::from(s), + None => usage(), + } +}