From 7df3bfeb9139c4a6256364021e604c0fe312382b Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 18 Oct 2024 19:16:25 +0300 Subject: [PATCH] initial commit --- .gitignore | 2 + ggemtext/Cargo.toml | 14 +++++ ggemtext/src/lib.rs | 19 ++++++ ggemtext/src/line.rs | 5 ++ ggemtext/src/line/code.rs | 25 ++++++++ ggemtext/src/line/code/inline.rs | 29 ++++++++++ ggemtext/src/line/code/multiline.rs | 46 +++++++++++++++ ggemtext/src/line/header.rs | 47 +++++++++++++++ ggemtext/src/line/link.rs | 89 +++++++++++++++++++++++++++++ ggemtext/src/line/list.rs | 29 ++++++++++ ggemtext/src/line/quote.rs | 29 ++++++++++ 11 files changed, 334 insertions(+) create mode 100644 .gitignore create mode 100644 ggemtext/Cargo.toml create mode 100644 ggemtext/src/lib.rs create mode 100644 ggemtext/src/line.rs create mode 100644 ggemtext/src/line/code.rs create mode 100644 ggemtext/src/line/code/inline.rs create mode 100644 ggemtext/src/line/code/multiline.rs create mode 100644 ggemtext/src/line/header.rs create mode 100644 ggemtext/src/line/link.rs create mode 100644 ggemtext/src/line/list.rs create mode 100644 ggemtext/src/line/quote.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f3de752 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +Cargo.lock +ggemtext/target \ No newline at end of file diff --git a/ggemtext/Cargo.toml b/ggemtext/Cargo.toml new file mode 100644 index 0000000..f028194 --- /dev/null +++ b/ggemtext/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "ggemtext" +version = "0.1.0" +edition = "2021" +license = "MIT" +readme = "README.md" +description = "Glib-oriented Gemtext API" +keywords = ["gemtext", "gemini", "gemini-protocol", "gtk", "glib"] +categories = ["network-programming"] +repository = "https://github.com/YGGverse/ggemtext" + +[dependencies.gtk] +package = "gtk4" +version = "0.9.1" diff --git a/ggemtext/src/lib.rs b/ggemtext/src/lib.rs new file mode 100644 index 0000000..f1c93ba --- /dev/null +++ b/ggemtext/src/lib.rs @@ -0,0 +1,19 @@ +mod line; + +#[cfg(test)] +mod tests { + use super::line::header::{Header, Level}; + + #[test] + fn h1() { + match Header::from("# H1") { + Some(h1) => { + assert_eq!(h1.level as i32, Level::H1 as i32); // @TODO + assert_eq!(h1.value, "H1"); + } + None => assert!(false), + }; + } + + // @TODO other tags +} diff --git a/ggemtext/src/line.rs b/ggemtext/src/line.rs new file mode 100644 index 0000000..fd70850 --- /dev/null +++ b/ggemtext/src/line.rs @@ -0,0 +1,5 @@ +pub mod code; +pub mod header; +pub mod link; +pub mod list; +pub mod quote; diff --git a/ggemtext/src/line/code.rs b/ggemtext/src/line/code.rs new file mode 100644 index 0000000..f9ab3ee --- /dev/null +++ b/ggemtext/src/line/code.rs @@ -0,0 +1,25 @@ +pub mod inline; +pub mod multiline; + +use inline::Inline; +use multiline::Multiline; + +pub struct Code { + // nothing yet.. +} + +impl Code { + // Inline + pub fn inline_from(line: &str) -> Option { + Inline::from(line) + } + + // Multiline + pub fn multiline_begin_from(line: &str) -> Option { + Multiline::begin_from(line) + } + + pub fn multiline_continue_from(this: &mut Multiline, line: &str) { + Multiline::continue_from(this, line) + } +} diff --git a/ggemtext/src/line/code/inline.rs b/ggemtext/src/line/code/inline.rs new file mode 100644 index 0000000..7baddb8 --- /dev/null +++ b/ggemtext/src/line/code/inline.rs @@ -0,0 +1,29 @@ +use gtk::glib::{GString, Regex, RegexCompileFlags, RegexMatchFlags}; + +pub struct Inline { + pub value: GString, +} + +impl Inline { + pub fn from(line: &str) -> Option { + // Parse line + let regex = Regex::split_simple( + r"^`{3}([^`]*)`{3}$", + line, + RegexCompileFlags::DEFAULT, + RegexMatchFlags::DEFAULT, + ); + + // Detect value + let value = regex.get(1)?; + + if value.trim().is_empty() { + return None; + } + + // Result + Some(Self { + value: GString::from(value.as_str()), + }) + } +} diff --git a/ggemtext/src/line/code/multiline.rs b/ggemtext/src/line/code/multiline.rs new file mode 100644 index 0000000..96d7c72 --- /dev/null +++ b/ggemtext/src/line/code/multiline.rs @@ -0,0 +1,46 @@ +use gtk::glib::GString; + +pub struct Multiline { + pub alt: Option, + pub buffer: Vec, + pub completed: bool, +} + +impl Multiline { + // Search in line for tag open, + // return Self constructed on success or None + pub fn begin_from(line: &str) -> Option { + if line.starts_with("```") { + let alt = line.trim_start_matches("```"); + + return Some(Self { + alt: match alt.trim().is_empty() { + true => None, + false => Some(GString::from(alt)), + }, + buffer: Vec::new(), + completed: false, + }); + } + + None + } + + // Continue preformatted buffer from line, + // set `completed` as True on close tag found + pub fn continue_from(&mut self, line: &str) { + // Make sure buffer not completed yet + if self.completed { + panic!("Could not continue as completed") // @TODO handle + } + + // Line contain close tag + if line.ends_with("```") { + self.completed = true; + } + + // Append data to the buffer, trim close tag on exists + self.buffer + .push(GString::from(line.trim_end_matches("```"))); + } +} diff --git a/ggemtext/src/line/header.rs b/ggemtext/src/line/header.rs new file mode 100644 index 0000000..24dc248 --- /dev/null +++ b/ggemtext/src/line/header.rs @@ -0,0 +1,47 @@ +use gtk::glib::{GString, Regex, RegexCompileFlags, RegexMatchFlags}; + +pub enum Level { + H1, + H2, + H3, +} + +pub struct Header { + pub value: GString, + pub level: Level, +} + +impl Header { + pub fn from(line: &str) -> Option { + // Parse line + let regex = Regex::split_simple( + r"^(#{1,3})\s*(.+)$", + line, + RegexCompileFlags::DEFAULT, + RegexMatchFlags::DEFAULT, + ); + + // Detect header level + let level = regex.get(1)?; + + let level = match level.len() { + 1 => Level::H1, + 2 => Level::H2, + 3 => Level::H3, + _ => return None, + }; + + // Detect header value + let value = regex.get(2)?; + + if value.trim().is_empty() { + return None; + } + + // Result + Some(Self { + level, + value: GString::from(value.as_str()), + }) + } +} diff --git a/ggemtext/src/line/link.rs b/ggemtext/src/line/link.rs new file mode 100644 index 0000000..b1d99e2 --- /dev/null +++ b/ggemtext/src/line/link.rs @@ -0,0 +1,89 @@ +use gtk::glib::{ + DateTime, GString, Regex, RegexCompileFlags, RegexMatchFlags, TimeZone, Uri, UriFlags, +}; + +pub struct Link { + pub alt: Option, // [optional] alternative link description + pub is_external: Option, // [optional] external link indication, on base option provided + pub timestamp: Option, // [optional] valid link DateTime object + pub uri: Uri, // [required] valid link URI object +} + +impl Link { + pub fn from(line: &str, base: Option<&Uri>, timezone: Option<&TimeZone>) -> Option { + // Define initial values + let mut alt = None; + let mut timestamp = None; + let mut is_external = None; + + // Begin line parse + let regex = Regex::split_simple( + r"^=>\s*([^\s]+)\s*(\d{4}-\d{2}-\d{2})?\s*(.+)?$", + line, + RegexCompileFlags::DEFAULT, + RegexMatchFlags::DEFAULT, + ); + + // Detect address required to continue + let unresolved_address = regex.get(1)?; + + // Convert address to the valid URI + let uri = match base { + // Base conversion requested + Some(base_uri) => { + // Convert relative address to absolute + match Uri::resolve_relative( + Some(&base_uri.to_str()), + unresolved_address.as_str(), + UriFlags::NONE, + ) { + Ok(resolved_str) => { + // Try convert string to the valid URI + match Uri::parse(&resolved_str, UriFlags::NONE) { + Ok(resolved_uri) => { + // Change external status + is_external = Some(resolved_uri.scheme() != base_uri.scheme()); + + // Result + resolved_uri + } + Err(_) => return None, + } + } + Err(_) => return None, + } + } + // Base resolve not requested + None => { + // Just try convert address to valid URI + match Uri::parse(&unresolved_address, UriFlags::NONE) { + Ok(unresolved_uri) => unresolved_uri, + Err(_) => return None, + } + } + }; + + // Timestamp + if let Some(date) = regex.get(2) { + // @TODO even possible, but simpler to work with `DateTime` API + // await for new features in `Date` as better in Gemini context + // https://docs.gtk.org/glib/struct.Date.html + timestamp = match DateTime::from_iso8601(&format!("{date}T00:00:00"), timezone) { + Ok(value) => Some(value), + Err(_) => None, + } + } + + // Alt + if let Some(value) = regex.get(3) { + alt = Some(GString::from(value.as_str())) + }; + + Some(Self { + alt, + is_external, + timestamp, + uri, + }) + } +} diff --git a/ggemtext/src/line/list.rs b/ggemtext/src/line/list.rs new file mode 100644 index 0000000..f2598e5 --- /dev/null +++ b/ggemtext/src/line/list.rs @@ -0,0 +1,29 @@ +use gtk::glib::{GString, Regex, RegexCompileFlags, RegexMatchFlags}; + +pub struct List { + pub value: GString, +} + +impl List { + pub fn from(line: &str) -> Option { + // Parse line + let regex = Regex::split_simple( + r"^\*\s*(.+)$", + line, + RegexCompileFlags::DEFAULT, + RegexMatchFlags::DEFAULT, + ); + + // Detect value + let value = regex.get(1)?; + + if value.trim().is_empty() { + return None; + } + + // Result + Some(Self { + value: GString::from(value.as_str()), + }) + } +} diff --git a/ggemtext/src/line/quote.rs b/ggemtext/src/line/quote.rs new file mode 100644 index 0000000..20d69b6 --- /dev/null +++ b/ggemtext/src/line/quote.rs @@ -0,0 +1,29 @@ +use gtk::glib::{GString, Regex, RegexCompileFlags, RegexMatchFlags}; + +pub struct Quote { + pub value: GString, +} + +impl Quote { + pub fn from(line: &str) -> Option { + // Parse line + let regex = Regex::split_simple( + r"^>\s*(.+)$", + line, + RegexCompileFlags::DEFAULT, + RegexMatchFlags::DEFAULT, + ); + + // Detect value + let value = regex.get(1)?; + + if value.trim().is_empty() { + return None; + } + + // Result + Some(Self { + value: GString::from(value.as_str()), + }) + } +}