mirror of
https://github.com/YGGverse/ggemtext.git
synced 2026-03-31 17:15:33 +00:00
initial commit
This commit is contained in:
parent
c608e711c4
commit
7df3bfeb91
11 changed files with 334 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
Cargo.lock
|
||||
ggemtext/target
|
||||
14
ggemtext/Cargo.toml
Normal file
14
ggemtext/Cargo.toml
Normal file
|
|
@ -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"
|
||||
19
ggemtext/src/lib.rs
Normal file
19
ggemtext/src/lib.rs
Normal file
|
|
@ -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
|
||||
}
|
||||
5
ggemtext/src/line.rs
Normal file
5
ggemtext/src/line.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
pub mod code;
|
||||
pub mod header;
|
||||
pub mod link;
|
||||
pub mod list;
|
||||
pub mod quote;
|
||||
25
ggemtext/src/line/code.rs
Normal file
25
ggemtext/src/line/code.rs
Normal file
|
|
@ -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> {
|
||||
Inline::from(line)
|
||||
}
|
||||
|
||||
// Multiline
|
||||
pub fn multiline_begin_from(line: &str) -> Option<Multiline> {
|
||||
Multiline::begin_from(line)
|
||||
}
|
||||
|
||||
pub fn multiline_continue_from(this: &mut Multiline, line: &str) {
|
||||
Multiline::continue_from(this, line)
|
||||
}
|
||||
}
|
||||
29
ggemtext/src/line/code/inline.rs
Normal file
29
ggemtext/src/line/code/inline.rs
Normal file
|
|
@ -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<Self> {
|
||||
// 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()),
|
||||
})
|
||||
}
|
||||
}
|
||||
46
ggemtext/src/line/code/multiline.rs
Normal file
46
ggemtext/src/line/code/multiline.rs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
use gtk::glib::GString;
|
||||
|
||||
pub struct Multiline {
|
||||
pub alt: Option<GString>,
|
||||
pub buffer: Vec<GString>,
|
||||
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<Self> {
|
||||
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("```")));
|
||||
}
|
||||
}
|
||||
47
ggemtext/src/line/header.rs
Normal file
47
ggemtext/src/line/header.rs
Normal file
|
|
@ -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<Self> {
|
||||
// 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()),
|
||||
})
|
||||
}
|
||||
}
|
||||
89
ggemtext/src/line/link.rs
Normal file
89
ggemtext/src/line/link.rs
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
use gtk::glib::{
|
||||
DateTime, GString, Regex, RegexCompileFlags, RegexMatchFlags, TimeZone, Uri, UriFlags,
|
||||
};
|
||||
|
||||
pub struct Link {
|
||||
pub alt: Option<GString>, // [optional] alternative link description
|
||||
pub is_external: Option<bool>, // [optional] external link indication, on base option provided
|
||||
pub timestamp: Option<DateTime>, // [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<Self> {
|
||||
// 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
29
ggemtext/src/line/list.rs
Normal file
29
ggemtext/src/line/list.rs
Normal file
|
|
@ -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<Self> {
|
||||
// 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()),
|
||||
})
|
||||
}
|
||||
}
|
||||
29
ggemtext/src/line/quote.rs
Normal file
29
ggemtext/src/line/quote.rs
Normal file
|
|
@ -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<Self> {
|
||||
// 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()),
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue