mirror of
https://github.com/YGGverse/ggemtext.git
synced 2026-03-31 09:05:32 +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