From 02e8e8a06b1e2f37056702083b3c428f1222ec35 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 19 Oct 2024 13:32:13 +0300 Subject: [PATCH 001/105] enshort --- src/lib.rs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index da546e7..29f7200 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,9 +14,7 @@ mod tests { fn line() { // Code match Code::inline_from("```inline```") { - Some(inline) => { - assert_eq!(inline.value, "inline"); - } + Some(inline) => assert_eq!(inline.value, "inline"), None => assert!(false), }; @@ -70,17 +68,13 @@ mod tests { // List match List::from("* Item") { - Some(list) => { - assert_eq!(list.value, "Item"); - } + Some(list) => assert_eq!(list.value, "Item"), None => assert!(false), }; // Quote match Quote::from("> Quote") { - Some(quote) => { - assert_eq!(quote.value, "Quote"); - } + Some(quote) => assert_eq!(quote.value, "Quote"), None => assert!(false), }; } From 07ac1328d541b29bdc6f32c83411a70edc6ef232 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 19 Oct 2024 14:17:06 +0300 Subject: [PATCH 002/105] fix date assertion --- src/lib.rs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 29f7200..bb41261 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,14 +57,30 @@ mod tests { }; // Link - match Link::from("=> gemini://geminiprotocol.net Gemini", None, None) { + match Link::from( + "=> gemini://geminiprotocol.net 1965-01-19 Gemini", + None, // absolute path given, base not wanted + Some(>k::glib::TimeZone::local()), + ) { Some(link) => { + // Alt assert_eq!(link.alt, Some("Gemini".into())); + + // Date + match link.timestamp { + Some(timestamp) => { + assert_eq!(timestamp.year(), 1965); + assert_eq!(timestamp.month(), 01); + assert_eq!(timestamp.day_of_month(), 19); + } + None => assert!(false), + } + + // URI assert_eq!(link.uri.to_string(), "gemini://geminiprotocol.net"); - // @TODO timestamp } None => assert!(false), - }; // @TODO options + }; // List match List::from("* Item") { From a71a17b92000cf49b35e7bc69481cf5f4ed2756a Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 19 Oct 2024 14:20:38 +0300 Subject: [PATCH 003/105] add basic documentation --- README.md | 121 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/README.md b/README.md index b3dbcb1..5ac1f1e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,124 @@ # ggemtext Glib-oriented [Gemtext](https://geminiprotocol.net/docs/gemtext.gmi) API + +## Install + +``` bash +cargo add ggemtext +``` + +## Usage + +### Line + +Line parser, useful for [TextTag](https://docs.gtk.org/gtk4/class.TextTag.html) operations in [TextBuffer](https://docs.gtk.org/gtk4/class.TextBuffer.html) context. + +**Connect dependencies** + +``` rust +use ggemtext::line::{ + code::Code, + header::{Header, Level}, + link::Link, + list::List, + quote::Quote, +}; +``` + +**Prepare document** + +Iterate Gemtext lines to continue with [Line](#Line) API: + +``` rust +for line in gemtext.lines() { + // .. +} +``` + +#### Code + +##### Inline + +``` rust +match Code::inline_from("```inline```") { + Some(inline) => assert_eq!(inline.value, "inline"), + None => assert!(false), +}; +``` + +##### Multiline + +``` rust +match Code::multiline_begin_from("```alt") { + Some(mut multiline) => { + Code::multiline_continue_from(&mut multiline, "line 1"); + Code::multiline_continue_from(&mut multiline, "line 2"); + Code::multiline_continue_from(&mut multiline, "```"); // complete + + assert!(multiline.completed); + assert_eq!(multiline.alt, Some("alt".into())); + assert_eq!(multiline.buffer.len(), 3); + } + None => assert!(false), +}; +``` + +#### Header + +``` rust +match Header::from("# H1") { + Some(h1) => { + assert_eq!(h1.level as i8, Level::H1 as i8); + assert_eq!(h1.value, "H1"); + } + None => assert!(false), +}; // H1, H2, H3 +``` + +#### Link + +``` rust +match Link::from( + "=> gemini://geminiprotocol.net 1965-01-19 Gemini", + None, // absolute path given, base not wanted + Some(>k::glib::TimeZone::local()), +) { + Some(link) => { + // Alt + assert_eq!(link.alt, Some("Gemini".into())); + + // Date + match link.timestamp { + Some(timestamp) => { + assert_eq!(timestamp.year(), 1965); + assert_eq!(timestamp.month(), 01); + assert_eq!(timestamp.day_of_month(), 19); + } + None => assert!(false), + } + + // URI + assert_eq!(link.uri.to_string(), "gemini://geminiprotocol.net"); + } + None => assert!(false), +}; +``` + +#### List + +``` rust +match List::from("* Item") { + Some(list) => assert_eq!(list.value, "Item"), + None => assert!(false), +}; +``` + +#### Quote + +``` rust +match Quote::from("> Quote") { + Some(quote) => assert_eq!(quote.value, "Quote"), + None => assert!(false), +}; +``` \ No newline at end of file From c820ae0b96422a0d77d9a62f6f8952b0c84febe6 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 19 Oct 2024 14:33:08 +0300 Subject: [PATCH 004/105] add test document --- tests/document.gmi | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 tests/document.gmi diff --git a/tests/document.gmi b/tests/document.gmi new file mode 100644 index 0000000..c659ad8 --- /dev/null +++ b/tests/document.gmi @@ -0,0 +1,25 @@ +# H1 +## H2 +### H3 + +=> gemini://geminiprotocol.net +=> gemini://geminiprotocol.net 1965-01-19 +=> gemini://geminiprotocol.net Gemini + +* Listing item 1 +* Listing item 2 +* Listing item 3 + +```inline code``` + +``` alt text +multi + line code +``` + +> quoted string + +paragraph line 1 +paragraph line 2 + +paragraph 2 \ No newline at end of file From 541683808c130d92861882dbc6d96a55b17286e1 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 20 Oct 2024 00:19:47 +0300 Subject: [PATCH 005/105] add integration test --- tests/{document.gmi => integration_test.gmi} | 7 ++ tests/integration_test.rs | 92 ++++++++++++++++++++ 2 files changed, 99 insertions(+) rename tests/{document.gmi => integration_test.gmi} (71%) create mode 100644 tests/integration_test.rs diff --git a/tests/document.gmi b/tests/integration_test.gmi similarity index 71% rename from tests/document.gmi rename to tests/integration_test.gmi index c659ad8..e717752 100644 --- a/tests/document.gmi +++ b/tests/integration_test.gmi @@ -5,6 +5,8 @@ => gemini://geminiprotocol.net => gemini://geminiprotocol.net 1965-01-19 => gemini://geminiprotocol.net Gemini +=> gemini://geminiprotocol.net 1965-01-19 Gemini +=> /docs/gemtext.gmi 1965-01-19 Gemini * Listing item 1 * Listing item 2 @@ -17,6 +19,11 @@ multi line code ``` +``` +alt-less + line code +``` + > quoted string paragraph line 1 diff --git a/tests/integration_test.rs b/tests/integration_test.rs new file mode 100644 index 0000000..c5e1f31 --- /dev/null +++ b/tests/integration_test.rs @@ -0,0 +1,92 @@ +use ggemtext::line::{ + code::{inline::Inline, multiline::Multiline, Code}, + header::{Header, Level}, + link::Link, + list::List, + quote::Quote, +}; + +#[test] +fn gemtext() { + match std::fs::read_to_string("tests/integration_test.gmi") { + Ok(gemtext) => { + // Init tags collection + let mut code_inline: Vec = Vec::new(); + let mut code_multiline: Vec = Vec::new(); + let mut header: Vec
= Vec::new(); + let mut link: Vec = Vec::new(); + let mut list: Vec = Vec::new(); + let mut quote: Vec = Vec::new(); + + // Define preformatted buffer + let mut code_multiline_buffer: Option = None; + + // Define base URI as integration_test.gmi contain one relative link + let base = match gtk::glib::Uri::parse( + "gemini://geminiprotocol.net", + gtk::glib::UriFlags::NONE, + ) { + Ok(uri) => Some(uri), + Err(_) => None, + }; + + // Parse document by line + for line in gemtext.lines() { + // Inline code + if let Some(result) = Code::inline_from(line) { + code_inline.push(result); + } + + // Multiline code + match code_multiline_buffer { + None => { + if let Some(code) = Code::multiline_begin_from(line) { + code_multiline_buffer = Some(code); + } + } + Some(ref mut result) => { + Code::multiline_continue_from(result, line); + if result.completed { + code_multiline.push(code_multiline_buffer.take().unwrap()); + code_multiline_buffer = None; + } + } + }; + + // Header + if let Some(result) = Header::from(line) { + header.push(result); + } + + // Link + if let Some(result) = + Link::from(line, base.as_ref(), Some(>k::glib::TimeZone::local())) + { + link.push(result); + } + + // List + if let Some(result) = List::from(line) { + list.push(result); + } + + // Quote + if let Some(result) = Quote::from(line) { + quote.push(result); + } + } + + // Assert tags quantity + assert_eq!(code_inline.len(), 1); + assert_eq!(code_multiline.len(), 2); + assert_eq!(header.len(), 3); + assert_eq!(link.len(), 5); + assert_eq!(list.len(), 3); + assert_eq!(quote.len(), 1); + } + // Could not load gemtext file + Err(_) => { + assert!(false); + } + } +} From 6f3d4e0f5820fe0358f895894ba291a9bc28f346 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 20 Oct 2024 00:29:34 +0300 Subject: [PATCH 006/105] use namespace --- tests/integration_test.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/integration_test.rs b/tests/integration_test.rs index c5e1f31..375c383 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -6,9 +6,12 @@ use ggemtext::line::{ quote::Quote, }; +use gtk::glib::{TimeZone, Uri, UriFlags}; +use std::fs::read_to_string; + #[test] fn gemtext() { - match std::fs::read_to_string("tests/integration_test.gmi") { + match read_to_string("tests/integration_test.gmi") { Ok(gemtext) => { // Init tags collection let mut code_inline: Vec = Vec::new(); @@ -22,14 +25,14 @@ fn gemtext() { let mut code_multiline_buffer: Option = None; // Define base URI as integration_test.gmi contain one relative link - let base = match gtk::glib::Uri::parse( - "gemini://geminiprotocol.net", - gtk::glib::UriFlags::NONE, - ) { + let base = match Uri::parse("gemini://geminiprotocol.net", UriFlags::NONE) { Ok(uri) => Some(uri), Err(_) => None, }; + // Define timezone as integration_test.gmi contain one links with date + let timezone = Some(TimeZone::local()); + // Parse document by line for line in gemtext.lines() { // Inline code @@ -59,9 +62,7 @@ fn gemtext() { } // Link - if let Some(result) = - Link::from(line, base.as_ref(), Some(>k::glib::TimeZone::local())) - { + if let Some(result) = Link::from(line, base.as_ref(), timezone.as_ref()) { link.push(result); } From 321c42876569b3de652d69ff4ad2cbff238aa7b3 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 20 Oct 2024 00:31:16 +0300 Subject: [PATCH 007/105] update namespace --- tests/integration_test.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 375c383..e5123c7 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -7,11 +7,11 @@ use ggemtext::line::{ }; use gtk::glib::{TimeZone, Uri, UriFlags}; -use std::fs::read_to_string; +use std::fs; #[test] fn gemtext() { - match read_to_string("tests/integration_test.gmi") { + match fs::read_to_string("tests/integration_test.gmi") { Ok(gemtext) => { // Init tags collection let mut code_inline: Vec = Vec::new(); From 9373faabb8989c8e9a636f33abf5a677bbe2899f Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 20 Oct 2024 11:28:51 +0300 Subject: [PATCH 008/105] add empty value validation --- src/line/link.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/line/link.rs b/src/line/link.rs index b1d99e2..09543b3 100644 --- a/src/line/link.rs +++ b/src/line/link.rs @@ -76,7 +76,9 @@ impl Link { // Alt if let Some(value) = regex.get(3) { - alt = Some(GString::from(value.as_str())) + if !value.is_empty() { + alt = Some(GString::from(value.as_str())) + } }; Some(Self { From 77783af9b0de4888d1129164f229b3fce7b41765 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 20 Oct 2024 11:44:10 +0300 Subject: [PATCH 009/105] move integration test to separated file --- src/lib.rs | 96 -------- .../{integration_test.gmi => integration.gmi} | 5 +- tests/integration.rs | 217 ++++++++++++++++++ tests/integration_test.rs | 93 -------- 4 files changed, 219 insertions(+), 192 deletions(-) rename tests/{integration_test.gmi => integration.gmi} (89%) create mode 100644 tests/integration.rs delete mode 100644 tests/integration_test.rs diff --git a/src/lib.rs b/src/lib.rs index bb41261..7f6b32b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,97 +1 @@ pub mod line; - -#[cfg(test)] -mod tests { - use super::line::{ - code::Code, - header::{Header, Level}, - link::Link, - list::List, - quote::Quote, - }; - - #[test] - fn line() { - // Code - match Code::inline_from("```inline```") { - Some(inline) => assert_eq!(inline.value, "inline"), - None => assert!(false), - }; - - match Code::multiline_begin_from("```alt") { - Some(mut multiline) => { - Code::multiline_continue_from(&mut multiline, "line 1"); - Code::multiline_continue_from(&mut multiline, "line 2"); - Code::multiline_continue_from(&mut multiline, "```"); - - assert!(multiline.completed); - assert_eq!(multiline.alt, Some("alt".into())); - assert_eq!(multiline.buffer.len(), 3); - } - None => assert!(false), - }; - - // Header - match Header::from("# H1") { - Some(h1) => { - assert_eq!(h1.level as i8, Level::H1 as i8); - assert_eq!(h1.value, "H1"); - } - None => assert!(false), - }; - - match Header::from("## H2") { - Some(h2) => { - assert_eq!(h2.level as i8, Level::H2 as i8); - assert_eq!(h2.value, "H2"); - } - None => assert!(false), - }; - - match Header::from("### H3") { - Some(h3) => { - assert_eq!(h3.level as i8, Level::H3 as i8); - assert_eq!(h3.value, "H3"); - } - None => assert!(false), - }; - - // Link - match Link::from( - "=> gemini://geminiprotocol.net 1965-01-19 Gemini", - None, // absolute path given, base not wanted - Some(>k::glib::TimeZone::local()), - ) { - Some(link) => { - // Alt - assert_eq!(link.alt, Some("Gemini".into())); - - // Date - match link.timestamp { - Some(timestamp) => { - assert_eq!(timestamp.year(), 1965); - assert_eq!(timestamp.month(), 01); - assert_eq!(timestamp.day_of_month(), 19); - } - None => assert!(false), - } - - // URI - assert_eq!(link.uri.to_string(), "gemini://geminiprotocol.net"); - } - None => assert!(false), - }; - - // List - match List::from("* Item") { - Some(list) => assert_eq!(list.value, "Item"), - None => assert!(false), - }; - - // Quote - match Quote::from("> Quote") { - Some(quote) => assert_eq!(quote.value, "Quote"), - None => assert!(false), - }; - } -} diff --git a/tests/integration_test.gmi b/tests/integration.gmi similarity index 89% rename from tests/integration_test.gmi rename to tests/integration.gmi index e717752..b7a7843 100644 --- a/tests/integration_test.gmi +++ b/tests/integration.gmi @@ -10,18 +10,17 @@ * Listing item 1 * Listing item 2 -* Listing item 3 ```inline code``` ``` alt text multi - line code + preformatted line ``` ``` alt-less - line code + preformatted line ``` > quoted string diff --git a/tests/integration.rs b/tests/integration.rs new file mode 100644 index 0000000..75f46bc --- /dev/null +++ b/tests/integration.rs @@ -0,0 +1,217 @@ +use ggemtext::line::{ + code::{inline::Inline, multiline::Multiline, Code}, + header::{Header, Level}, + link::Link, + list::List, + quote::Quote, +}; + +use gtk::glib::{TimeZone, Uri, UriFlags}; +use std::fs; + +#[test] +fn gemtext() { + match fs::read_to_string("tests/integration.gmi") { + Ok(gemtext) => { + // Init tags collection + let mut code_inline: Vec = Vec::new(); + let mut code_multiline: Vec = Vec::new(); + let mut header: Vec
= Vec::new(); + let mut link: Vec = Vec::new(); + let mut list: Vec = Vec::new(); + let mut quote: Vec = Vec::new(); + + // Define preformatted buffer + let mut code_multiline_buffer: Option = None; + + // Define base URI as integration.gmi contain one relative link + let base = match Uri::parse("gemini://geminiprotocol.net", UriFlags::NONE) { + Ok(uri) => Some(uri), + Err(_) => None, + }; + + // Define timezone as integration.gmi contain one links with date + let timezone = Some(TimeZone::local()); + + // Parse document by line + for line in gemtext.lines() { + // Inline code + if let Some(result) = Code::inline_from(line) { + code_inline.push(result); + continue; + } + + // Multiline code + match code_multiline_buffer { + None => { + if let Some(code) = Code::multiline_begin_from(line) { + code_multiline_buffer = Some(code); + continue; + } + } + Some(ref mut result) => { + Code::multiline_continue_from(result, line); + if result.completed { + code_multiline.push(code_multiline_buffer.take().unwrap()); + code_multiline_buffer = None; + } + continue; + } + }; + + // Header + if let Some(result) = Header::from(line) { + header.push(result); + continue; + } + + // Link + if let Some(result) = Link::from(line, base.as_ref(), timezone.as_ref()) { + link.push(result); + continue; + } + + // List + if let Some(result) = List::from(line) { + list.push(result); + continue; + } + + // Quote + if let Some(result) = Quote::from(line) { + quote.push(result); + continue; + } + } + + // Validate inline code + assert_eq!(code_inline.len(), 1); + assert_eq!(code_inline.get(0).unwrap().value, "inline code"); + + // Validate multiline code + assert_eq!(code_multiline.len(), 2); + + { + let item = code_multiline.get(0).unwrap(); + assert_eq!(item.alt.clone().unwrap(), " alt text"); + assert_eq!(item.buffer.len(), 3); + assert_eq!(item.buffer.get(0).unwrap(), "multi"); + assert_eq!(item.buffer.get(1).unwrap(), " preformatted line"); + } // #1 + + { + let item = code_multiline.get(1).unwrap(); + assert_eq!(item.alt.clone(), None); + assert_eq!(item.buffer.len(), 3); + assert_eq!(item.buffer.get(0).unwrap(), "alt-less"); + assert_eq!(item.buffer.get(1).unwrap(), " preformatted line"); + } // #2 + + // Validate headers + assert_eq!(header.len(), 3); + + fn to_i8(level: &Level) -> i8 { + match level { + Level::H1 => 1, + Level::H2 => 2, + Level::H3 => 3, + } + } // comparison helper + + { + let item = header.get(0).unwrap(); + + assert_eq!(to_i8(&item.level), to_i8(&Level::H1)); + assert_eq!(item.value, "H1"); + } // #1 + + { + let item = header.get(1).unwrap(); + + assert_eq!(to_i8(&item.level), to_i8(&Level::H2)); + assert_eq!(item.value, "H2"); + } // #2 + + { + let item = header.get(2).unwrap(); + + assert_eq!(to_i8(&item.level), to_i8(&Level::H3)); + assert_eq!(item.value, "H3"); + } // #3 + + // Validate links + assert_eq!(link.len(), 5); + + { + let item = link.get(0).unwrap(); + + assert_eq!(item.alt, None); + assert_eq!(item.timestamp, None); + assert_eq!(item.uri.to_str(), "gemini://geminiprotocol.net"); // @TODO len 27? + } // #1 + + { + let item = link.get(1).unwrap(); + + assert_eq!(item.alt, None); + + let timestamp = item.timestamp.clone().unwrap(); + assert_eq!(timestamp.year(), 1965); + assert_eq!(timestamp.month(), 01); + assert_eq!(timestamp.day_of_month(), 19); + + assert_eq!(item.uri.to_str(), "gemini://geminiprotocol.net"); + } // #2 + + { + let item = link.get(2).unwrap(); + + assert_eq!(item.alt.clone().unwrap(), "Gemini"); + assert_eq!(item.timestamp, None); + assert_eq!(item.uri.to_str(), "gemini://geminiprotocol.net"); + } // #3 + + { + let item = link.get(3).unwrap(); + + assert_eq!(item.alt.clone().unwrap(), "Gemini"); + + let timestamp = item.timestamp.clone().unwrap(); + assert_eq!(timestamp.year(), 1965); + assert_eq!(timestamp.month(), 01); + assert_eq!(timestamp.day_of_month(), 19); + + assert_eq!(item.uri.to_str(), "gemini://geminiprotocol.net"); + } // #4 + + { + let item = link.get(4).unwrap(); + + assert_eq!(item.alt.clone().unwrap(), "Gemini"); + + let timestamp = item.timestamp.clone().unwrap(); + assert_eq!(timestamp.year(), 1965); + assert_eq!(timestamp.month(), 01); + assert_eq!(timestamp.day_of_month(), 19); + + assert_eq!( + item.uri.to_str(), + "gemini://geminiprotocol.net/docs/gemtext.gmi" + ); + } // #5 + + // Validate lists + assert_eq!(list.len(), 2); + assert_eq!(list.get(0).unwrap().value, "Listing item 1"); + assert_eq!(list.get(1).unwrap().value, "Listing item 2"); + + // Validate quotes + assert_eq!(quote.len(), 1); + assert_eq!(quote.get(0).unwrap().value, "quoted string"); + } + // Could not load gemtext file + Err(_) => { + assert!(false); + } + } +} diff --git a/tests/integration_test.rs b/tests/integration_test.rs deleted file mode 100644 index e5123c7..0000000 --- a/tests/integration_test.rs +++ /dev/null @@ -1,93 +0,0 @@ -use ggemtext::line::{ - code::{inline::Inline, multiline::Multiline, Code}, - header::{Header, Level}, - link::Link, - list::List, - quote::Quote, -}; - -use gtk::glib::{TimeZone, Uri, UriFlags}; -use std::fs; - -#[test] -fn gemtext() { - match fs::read_to_string("tests/integration_test.gmi") { - Ok(gemtext) => { - // Init tags collection - let mut code_inline: Vec = Vec::new(); - let mut code_multiline: Vec = Vec::new(); - let mut header: Vec
= Vec::new(); - let mut link: Vec = Vec::new(); - let mut list: Vec = Vec::new(); - let mut quote: Vec = Vec::new(); - - // Define preformatted buffer - let mut code_multiline_buffer: Option = None; - - // Define base URI as integration_test.gmi contain one relative link - let base = match Uri::parse("gemini://geminiprotocol.net", UriFlags::NONE) { - Ok(uri) => Some(uri), - Err(_) => None, - }; - - // Define timezone as integration_test.gmi contain one links with date - let timezone = Some(TimeZone::local()); - - // Parse document by line - for line in gemtext.lines() { - // Inline code - if let Some(result) = Code::inline_from(line) { - code_inline.push(result); - } - - // Multiline code - match code_multiline_buffer { - None => { - if let Some(code) = Code::multiline_begin_from(line) { - code_multiline_buffer = Some(code); - } - } - Some(ref mut result) => { - Code::multiline_continue_from(result, line); - if result.completed { - code_multiline.push(code_multiline_buffer.take().unwrap()); - code_multiline_buffer = None; - } - } - }; - - // Header - if let Some(result) = Header::from(line) { - header.push(result); - } - - // Link - if let Some(result) = Link::from(line, base.as_ref(), timezone.as_ref()) { - link.push(result); - } - - // List - if let Some(result) = List::from(line) { - list.push(result); - } - - // Quote - if let Some(result) = Quote::from(line) { - quote.push(result); - } - } - - // Assert tags quantity - assert_eq!(code_inline.len(), 1); - assert_eq!(code_multiline.len(), 2); - assert_eq!(header.len(), 3); - assert_eq!(link.len(), 5); - assert_eq!(list.len(), 3); - assert_eq!(quote.len(), 1); - } - // Could not load gemtext file - Err(_) => { - assert!(false); - } - } -} From 9c8d087e4486a6c2ff9cafea6b51855ebfb13a45 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 20 Oct 2024 11:45:52 +0300 Subject: [PATCH 010/105] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 55e4789..e35e66d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemtext" -version = "0.1.1" +version = "0.1.2" edition = "2021" license = "MIT" readme = "README.md" From 65b051f23e6a8394a6faa7508856d17f81343caf Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 20 Oct 2024 11:57:23 +0300 Subject: [PATCH 011/105] remove comment --- tests/integration.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration.rs b/tests/integration.rs index 75f46bc..494a5b9 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -147,7 +147,7 @@ fn gemtext() { assert_eq!(item.alt, None); assert_eq!(item.timestamp, None); - assert_eq!(item.uri.to_str(), "gemini://geminiprotocol.net"); // @TODO len 27? + assert_eq!(item.uri.to_str(), "gemini://geminiprotocol.net"); } // #1 { From dbb443d361891077096e80db8ad24661d983459a Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 20 Oct 2024 12:01:09 +0300 Subject: [PATCH 012/105] trim alt text for multiline code --- src/line/code/multiline.rs | 4 ++-- tests/integration.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/line/code/multiline.rs b/src/line/code/multiline.rs index af92cf6..cef1518 100644 --- a/src/line/code/multiline.rs +++ b/src/line/code/multiline.rs @@ -11,10 +11,10 @@ impl Multiline { /// return Self constructed on success or None pub fn begin_from(line: &str) -> Option { if line.starts_with("```") { - let alt = line.trim_start_matches("```"); + let alt = line.trim_start_matches("```").trim(); return Some(Self { - alt: match alt.trim().is_empty() { + alt: match alt.is_empty() { true => None, false => Some(GString::from(alt)), }, diff --git a/tests/integration.rs b/tests/integration.rs index 494a5b9..475398a 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -93,7 +93,7 @@ fn gemtext() { { let item = code_multiline.get(0).unwrap(); - assert_eq!(item.alt.clone().unwrap(), " alt text"); + assert_eq!(item.alt.clone().unwrap(), "alt text"); assert_eq!(item.buffer.len(), 3); assert_eq!(item.buffer.get(0).unwrap(), "multi"); assert_eq!(item.buffer.get(1).unwrap(), " preformatted line"); From f12d052b47ed3269f3a12291ab16bd0bb8bdff5b Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 20 Oct 2024 12:03:36 +0300 Subject: [PATCH 013/105] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e35e66d..479de04 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemtext" -version = "0.1.2" +version = "0.1.3" edition = "2021" license = "MIT" readme = "README.md" From be813cf643514d7d19bf4d892bb9d535322eb3dc Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 20 Oct 2024 14:18:29 +0300 Subject: [PATCH 014/105] remove extra dependencies --- Cargo.toml | 9 +++++---- README.md | 2 +- src/line/code/inline.rs | 2 +- src/line/code/multiline.rs | 2 +- src/line/header.rs | 2 +- src/line/link.rs | 4 +--- src/line/list.rs | 2 +- src/line/quote.rs | 2 +- tests/integration.rs | 2 +- 9 files changed, 13 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 479de04..afc20d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemtext" -version = "0.1.3" +version = "0.2.0" edition = "2021" license = "MIT" readme = "README.md" @@ -15,6 +15,7 @@ categories = [ ] repository = "https://github.com/YGGverse/ggemtext" -[dependencies.gtk] -package = "gtk4" -version = "0.9.1" +[dependencies.glib] +package = "glib" +version = "0.20.4" +features = ["v2_66"] diff --git a/README.md b/README.md index 5ac1f1e..2e0dd85 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ match Header::from("# H1") { match Link::from( "=> gemini://geminiprotocol.net 1965-01-19 Gemini", None, // absolute path given, base not wanted - Some(>k::glib::TimeZone::local()), + Some(&glib::TimeZone::local()), ) { Some(link) => { // Alt diff --git a/src/line/code/inline.rs b/src/line/code/inline.rs index 7baddb8..9f3ef8d 100644 --- a/src/line/code/inline.rs +++ b/src/line/code/inline.rs @@ -1,4 +1,4 @@ -use gtk::glib::{GString, Regex, RegexCompileFlags, RegexMatchFlags}; +use glib::{GString, Regex, RegexCompileFlags, RegexMatchFlags}; pub struct Inline { pub value: GString, diff --git a/src/line/code/multiline.rs b/src/line/code/multiline.rs index cef1518..0180df1 100644 --- a/src/line/code/multiline.rs +++ b/src/line/code/multiline.rs @@ -1,4 +1,4 @@ -use gtk::glib::GString; +use glib::GString; pub struct Multiline { pub alt: Option, diff --git a/src/line/header.rs b/src/line/header.rs index 24dc248..35cfab4 100644 --- a/src/line/header.rs +++ b/src/line/header.rs @@ -1,4 +1,4 @@ -use gtk::glib::{GString, Regex, RegexCompileFlags, RegexMatchFlags}; +use glib::{GString, Regex, RegexCompileFlags, RegexMatchFlags}; pub enum Level { H1, diff --git a/src/line/link.rs b/src/line/link.rs index 09543b3..e38d683 100644 --- a/src/line/link.rs +++ b/src/line/link.rs @@ -1,6 +1,4 @@ -use gtk::glib::{ - DateTime, GString, Regex, RegexCompileFlags, RegexMatchFlags, TimeZone, Uri, UriFlags, -}; +use glib::{DateTime, GString, Regex, RegexCompileFlags, RegexMatchFlags, TimeZone, Uri, UriFlags}; pub struct Link { pub alt: Option, // [optional] alternative link description diff --git a/src/line/list.rs b/src/line/list.rs index f2598e5..2aa1320 100644 --- a/src/line/list.rs +++ b/src/line/list.rs @@ -1,4 +1,4 @@ -use gtk::glib::{GString, Regex, RegexCompileFlags, RegexMatchFlags}; +use glib::{GString, Regex, RegexCompileFlags, RegexMatchFlags}; pub struct List { pub value: GString, diff --git a/src/line/quote.rs b/src/line/quote.rs index 20d69b6..c19c475 100644 --- a/src/line/quote.rs +++ b/src/line/quote.rs @@ -1,4 +1,4 @@ -use gtk::glib::{GString, Regex, RegexCompileFlags, RegexMatchFlags}; +use glib::{GString, Regex, RegexCompileFlags, RegexMatchFlags}; pub struct Quote { pub value: GString, diff --git a/tests/integration.rs b/tests/integration.rs index 475398a..bbfb5d8 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -6,7 +6,7 @@ use ggemtext::line::{ quote::Quote, }; -use gtk::glib::{TimeZone, Uri, UriFlags}; +use glib::{TimeZone, Uri, UriFlags}; use std::fs; #[test] From dc9e6a14dcfcc3e06923e4564d8519e876d4c2b0 Mon Sep 17 00:00:00 2001 From: yggverse Date: Thu, 24 Oct 2024 20:49:01 +0300 Subject: [PATCH 015/105] update readme --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2e0dd85..6f8b311 100644 --- a/README.md +++ b/README.md @@ -121,4 +121,12 @@ match Quote::from("> Quote") { Some(quote) => assert_eq!(quote.value, "Quote"), None => assert!(false), }; -``` \ No newline at end of file +``` + +## Integrations + +* [Yoda](https://github.com/YGGverse/Yoda) - Browser for Gemini Protocol + +## See also + +* [ggemini](https://github.com/YGGverse/ggemini) - Glib-oriented client for Gemini Protocol \ No newline at end of file From 83d88ef69253230f99e425c0508a14ef6481da3f Mon Sep 17 00:00:00 2001 From: d47081 <108541346+d47081@users.noreply.github.com> Date: Fri, 1 Nov 2024 17:28:42 +0200 Subject: [PATCH 016/105] Create rust.yml --- .github/workflows/rust.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/rust.yml diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..9fd45e0 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,22 @@ +name: Rust + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test --verbose From 09ab2da4958302dbcccc007ae76deaaf1576499b Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 1 Nov 2024 17:32:28 +0200 Subject: [PATCH 017/105] update readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 6f8b311..dba4c65 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ cargo add ggemtext ## Usage +* [Documentation](https://docs.rs/ggemtext/latest/) + ### Line Line parser, useful for [TextTag](https://docs.gtk.org/gtk4/class.TextTag.html) operations in [TextBuffer](https://docs.gtk.org/gtk4/class.TextBuffer.html) context. From 5611d7ee5edf3bed38e3b6117e2b7aab1fb08356 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 1 Nov 2024 17:33:02 +0200 Subject: [PATCH 018/105] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dba4c65..b144a8e 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ cargo add ggemtext ## Usage -* [Documentation](https://docs.rs/ggemtext/latest/) +* [Documentation](https://docs.rs/ggemtext/latest/) (todo) ### Line From 127741351e1ba6264430000f652178c2e59065f8 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 1 Nov 2024 17:33:36 +0200 Subject: [PATCH 019/105] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b144a8e..dba4c65 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ cargo add ggemtext ## Usage -* [Documentation](https://docs.rs/ggemtext/latest/) (todo) +* [Documentation](https://docs.rs/ggemtext/latest/) ### Line From 93c7f738b7e0003b97f54742a982c9a9e8d01796 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 12 Nov 2024 07:22:11 +0200 Subject: [PATCH 020/105] remove extra reference --- src/line/link.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/line/link.rs b/src/line/link.rs index e38d683..5cbff40 100644 --- a/src/line/link.rs +++ b/src/line/link.rs @@ -54,7 +54,7 @@ impl Link { // Base resolve not requested None => { // Just try convert address to valid URI - match Uri::parse(&unresolved_address, UriFlags::NONE) { + match Uri::parse(unresolved_address, UriFlags::NONE) { Ok(unresolved_uri) => unresolved_uri, Err(_) => return None, } From dfb23931e31bd997527adaf8be6ebe4e9667f4a5 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 12 Nov 2024 07:25:36 +0200 Subject: [PATCH 021/105] fix protocol-relative URI links resolve #1 --- src/line/link.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/line/link.rs b/src/line/link.rs index 5cbff40..5716d53 100644 --- a/src/line/link.rs +++ b/src/line/link.rs @@ -23,7 +23,18 @@ impl Link { ); // Detect address required to continue - let unresolved_address = regex.get(1)?; + let mut unresolved_address = regex.get(1)?.to_string(); + + // Seems that [Uri resolver](https://docs.gtk.org/glib/type_func.Uri.resolve_relative.html) + // does not support [protocol-relative URI](https://datatracker.ietf.org/doc/html/rfc3986#section-4.2) + // resolve manually + if unresolved_address.starts_with("//:") { + let scheme = match base { + Some(base) => base.scheme(), + None => return None, + }; + unresolved_address = unresolved_address.replace("//:", &format!("{scheme}://")); + } // Convert address to the valid URI let uri = match base { @@ -54,7 +65,7 @@ impl Link { // Base resolve not requested None => { // Just try convert address to valid URI - match Uri::parse(unresolved_address, UriFlags::NONE) { + match Uri::parse(&unresolved_address, UriFlags::NONE) { Ok(unresolved_uri) => unresolved_uri, Err(_) => return None, } From 0c3fa0976961a2b15f73784cc5860517687a5a12 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 12 Nov 2024 07:32:42 +0200 Subject: [PATCH 022/105] add tests --- tests/integration.gmi | 1 + tests/integration.rs | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/integration.gmi b/tests/integration.gmi index b7a7843..0a8bb64 100644 --- a/tests/integration.gmi +++ b/tests/integration.gmi @@ -7,6 +7,7 @@ => gemini://geminiprotocol.net Gemini => gemini://geminiprotocol.net 1965-01-19 Gemini => /docs/gemtext.gmi 1965-01-19 Gemini +=> //:geminiprotocol.net * Listing item 1 * Listing item 2 diff --git a/tests/integration.rs b/tests/integration.rs index bbfb5d8..5aa2cd9 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -140,7 +140,7 @@ fn gemtext() { } // #3 // Validate links - assert_eq!(link.len(), 5); + assert_eq!(link.len(), 6); { let item = link.get(0).unwrap(); @@ -200,6 +200,14 @@ fn gemtext() { ); } // #5 + { + let item = link.get(5).unwrap(); + + assert_eq!(item.alt, None); + assert_eq!(item.timestamp, None); + assert_eq!(item.uri.to_str(), "gemini://geminiprotocol.net"); + } // #6 + // Validate lists assert_eq!(list.len(), 2); assert_eq!(list.get(0).unwrap().value, "Listing item 1"); From 7f6c45906532622be61cb5323220be1b1546953f Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 12 Nov 2024 07:33:20 +0200 Subject: [PATCH 023/105] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index afc20d2..b766dff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemtext" -version = "0.2.0" +version = "0.2.1" edition = "2021" license = "MIT" readme = "README.md" From 8c00d4bf89e701e8ee65209ff2f99f2fda04f4e2 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 2 Dec 2024 15:12:26 +0200 Subject: [PATCH 024/105] handle Code::multiline_continue_from result --- README.md | 6 +++--- src/line/code.rs | 9 +++++++-- src/line/code/error.rs | 16 ++++++++++++++++ src/line/code/multiline.rs | 9 +++++++-- src/line/code/multiline/error.rs | 16 ++++++++++++++++ tests/integration.rs | 2 +- 6 files changed, 50 insertions(+), 8 deletions(-) create mode 100644 src/line/code/error.rs create mode 100644 src/line/code/multiline/error.rs diff --git a/README.md b/README.md index dba4c65..0abc77a 100644 --- a/README.md +++ b/README.md @@ -54,9 +54,9 @@ match Code::inline_from("```inline```") { ``` rust match Code::multiline_begin_from("```alt") { Some(mut multiline) => { - Code::multiline_continue_from(&mut multiline, "line 1"); - Code::multiline_continue_from(&mut multiline, "line 2"); - Code::multiline_continue_from(&mut multiline, "```"); // complete + assert!(Code::multiline_continue_from(&mut multiline, "line 1").is_ok()); + assert!(Code::multiline_continue_from(&mut multiline, "line 2").is_ok()); + assert!(Code::multiline_continue_from(&mut multiline, "```").is_ok()); // complete assert!(multiline.completed); assert_eq!(multiline.alt, Some("alt".into())); diff --git a/src/line/code.rs b/src/line/code.rs index f9ab3ee..eb0c4df 100644 --- a/src/line/code.rs +++ b/src/line/code.rs @@ -1,6 +1,8 @@ +pub mod error; pub mod inline; pub mod multiline; +pub use error::Error; use inline::Inline; use multiline::Multiline; @@ -19,7 +21,10 @@ impl Code { Multiline::begin_from(line) } - pub fn multiline_continue_from(this: &mut Multiline, line: &str) { - Multiline::continue_from(this, line) + pub fn multiline_continue_from(this: &mut Multiline, line: &str) -> Result<(), Error> { + match Multiline::continue_from(this, line) { + Ok(()) => Ok(()), + Err(e) => Err(Error::Multiline(e)), + } } } diff --git a/src/line/code/error.rs b/src/line/code/error.rs new file mode 100644 index 0000000..410dfe3 --- /dev/null +++ b/src/line/code/error.rs @@ -0,0 +1,16 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Multiline(crate::line::code::multiline::Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Multiline(e) => { + write!(f, "Multiline error: {e}") + } + } + } +} diff --git a/src/line/code/multiline.rs b/src/line/code/multiline.rs index 0180df1..fe42870 100644 --- a/src/line/code/multiline.rs +++ b/src/line/code/multiline.rs @@ -1,3 +1,6 @@ +pub mod error; +pub use error::Error; + use glib::GString; pub struct Multiline { @@ -28,10 +31,10 @@ impl Multiline { /// Continue preformatted buffer from line, /// set `completed` as True on close tag found - pub fn continue_from(&mut self, line: &str) { + pub fn continue_from(&mut self, line: &str) -> Result<(), Error> { // Make sure buffer not completed yet if self.completed { - panic!("Could not continue as completed") // @TODO handle + return Err(Error::Completed); } // Line contain close tag @@ -42,5 +45,7 @@ impl Multiline { // Append data to the buffer, trim close tag on exists self.buffer .push(GString::from(line.trim_end_matches("```"))); + + Ok(()) } } diff --git a/src/line/code/multiline/error.rs b/src/line/code/multiline/error.rs new file mode 100644 index 0000000..78d0474 --- /dev/null +++ b/src/line/code/multiline/error.rs @@ -0,0 +1,16 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Completed, +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Completed => { + write!(f, "Could not continue as completed!") + } + } + } +} diff --git a/tests/integration.rs b/tests/integration.rs index 5aa2cd9..b41b8ba 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -50,7 +50,7 @@ fn gemtext() { } } Some(ref mut result) => { - Code::multiline_continue_from(result, line); + assert!(Code::multiline_continue_from(result, line).is_ok()); if result.completed { code_multiline.push(code_multiline_buffer.take().unwrap()); code_multiline_buffer = None; From 9b531bfd82a45704f87c1997aaf84e45c9fce199 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 2 Dec 2024 15:12:34 +0200 Subject: [PATCH 025/105] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index b766dff..565701e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemtext" -version = "0.2.1" +version = "0.3.0" edition = "2021" license = "MIT" readme = "README.md" From 5002fc392b01904d89051b2192eb67d3430daf0f Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 2 Dec 2024 20:04:27 +0200 Subject: [PATCH 026/105] drop extra vector, replace GString with String, use const for common values --- src/line/code/multiline.rs | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/line/code/multiline.rs b/src/line/code/multiline.rs index fe42870..9ca4bc6 100644 --- a/src/line/code/multiline.rs +++ b/src/line/code/multiline.rs @@ -1,11 +1,12 @@ pub mod error; pub use error::Error; -use glib::GString; +pub const NEW_LINE: char = '\n'; +pub const TAG: &str = "```"; pub struct Multiline { - pub alt: Option, - pub buffer: Vec, + pub alt: Option, + pub value: String, pub completed: bool, } @@ -13,15 +14,15 @@ 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("```").trim(); + if line.starts_with(TAG) { + let alt = line.trim_start_matches(TAG).trim(); return Some(Self { alt: match alt.is_empty() { true => None, - false => Some(GString::from(alt)), + false => Some(String::from(alt)), }, - buffer: Vec::new(), + value: String::new(), completed: false, }); } @@ -37,14 +38,15 @@ impl Multiline { return Err(Error::Completed); } - // Line contain close tag - if line.ends_with("```") { - self.completed = true; - } + // Append to value, trim close tag on exists + self.value.push_str(line.trim_end_matches(TAG)); - // Append data to the buffer, trim close tag on exists - self.buffer - .push(GString::from(line.trim_end_matches("```"))); + // Line contain close tag + if line.ends_with(TAG) { + self.completed = true; + } else { + self.value.push(NEW_LINE); + } Ok(()) } From 737117b91f3fcb2356843b55298fdb20bd5409ad Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 2 Dec 2024 20:17:31 +0200 Subject: [PATCH 027/105] update tests --- tests/integration.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/integration.rs b/tests/integration.rs index b41b8ba..5ba2cda 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -94,17 +94,17 @@ fn gemtext() { { let item = code_multiline.get(0).unwrap(); assert_eq!(item.alt.clone().unwrap(), "alt text"); - assert_eq!(item.buffer.len(), 3); - assert_eq!(item.buffer.get(0).unwrap(), "multi"); - assert_eq!(item.buffer.get(1).unwrap(), " preformatted line"); + assert_eq!(item.value.lines().count(), 2); + assert_eq!(item.value.lines().nth(0).unwrap(), "multi"); + assert_eq!(item.value.lines().nth(1).unwrap(), " preformatted line"); } // #1 { let item = code_multiline.get(1).unwrap(); assert_eq!(item.alt.clone(), None); - assert_eq!(item.buffer.len(), 3); - assert_eq!(item.buffer.get(0).unwrap(), "alt-less"); - assert_eq!(item.buffer.get(1).unwrap(), " preformatted line"); + assert_eq!(item.value.lines().count(), 2); + assert_eq!(item.value.lines().nth(0).unwrap(), "alt-less"); + assert_eq!(item.value.lines().nth(1).unwrap(), " preformatted line"); } // #2 // Validate headers From dbe080a4f10dc2ac47ce228ca493bd3e7421ad3e Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 2 Dec 2024 20:21:19 +0200 Subject: [PATCH 028/105] replace GString with String --- src/line/link.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/line/link.rs b/src/line/link.rs index 5716d53..99f4047 100644 --- a/src/line/link.rs +++ b/src/line/link.rs @@ -1,7 +1,7 @@ -use glib::{DateTime, GString, Regex, RegexCompileFlags, RegexMatchFlags, TimeZone, Uri, UriFlags}; +use glib::{DateTime, Regex, RegexCompileFlags, RegexMatchFlags, TimeZone, Uri, UriFlags}; pub struct Link { - pub alt: Option, // [optional] alternative link description + 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 @@ -86,7 +86,7 @@ impl Link { // Alt if let Some(value) = regex.get(3) { if !value.is_empty() { - alt = Some(GString::from(value.as_str())) + alt = Some(value.to_string()) } }; From 7d6c049870f7140f6656db7d36416d597b3195c6 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 2 Dec 2024 20:44:44 +0200 Subject: [PATCH 029/105] prepend new line before next lines only --- src/line/code/multiline.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/line/code/multiline.rs b/src/line/code/multiline.rs index 9ca4bc6..06ab1de 100644 --- a/src/line/code/multiline.rs +++ b/src/line/code/multiline.rs @@ -38,16 +38,19 @@ impl Multiline { return Err(Error::Completed); } - // Append to value, trim close tag on exists - self.value.push_str(line.trim_end_matches(TAG)); - // Line contain close tag if line.ends_with(TAG) { self.completed = true; - } else { + } + + // Prepend new line before next lines only + if !self.value.is_empty() { self.value.push(NEW_LINE); } + // Append to value, trim close tag on exists + self.value.push_str(line.trim_end_matches(TAG)); + Ok(()) } } From 2fd9327c88b63f44234f1111d386ee0fbc19f098 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 2 Dec 2024 22:00:52 +0200 Subject: [PATCH 030/105] replace GString with String --- src/line/code/inline.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/line/code/inline.rs b/src/line/code/inline.rs index 9f3ef8d..e52d03d 100644 --- a/src/line/code/inline.rs +++ b/src/line/code/inline.rs @@ -1,7 +1,7 @@ -use glib::{GString, Regex, RegexCompileFlags, RegexMatchFlags}; +use glib::{Regex, RegexCompileFlags, RegexMatchFlags}; pub struct Inline { - pub value: GString, + pub value: String, } impl Inline { @@ -23,7 +23,7 @@ impl Inline { // Result Some(Self { - value: GString::from(value.as_str()), + value: value.to_string(), }) } } From 1c95f8ad085c9bf51ac81b4dbf77aac1dda95306 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 3 Dec 2024 17:48:14 +0200 Subject: [PATCH 031/105] allow empty quote lines --- src/line/code/inline.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/line/code/inline.rs b/src/line/code/inline.rs index e52d03d..3919ab5 100644 --- a/src/line/code/inline.rs +++ b/src/line/code/inline.rs @@ -17,10 +17,6 @@ impl Inline { // Detect value let value = regex.get(1)?; - if value.trim().is_empty() { - return None; - } - // Result Some(Self { value: value.to_string(), From ec9b989d0a1b8cdc878421a35cbabe46ae1648ff Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 3 Dec 2024 17:55:19 +0200 Subject: [PATCH 032/105] allow empty line for quote tag only --- src/line/code/inline.rs | 4 ++++ src/line/quote.rs | 4 ---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/line/code/inline.rs b/src/line/code/inline.rs index 3919ab5..e52d03d 100644 --- a/src/line/code/inline.rs +++ b/src/line/code/inline.rs @@ -17,6 +17,10 @@ impl Inline { // Detect value let value = regex.get(1)?; + if value.trim().is_empty() { + return None; + } + // Result Some(Self { value: value.to_string(), diff --git a/src/line/quote.rs b/src/line/quote.rs index c19c475..9ec6d38 100644 --- a/src/line/quote.rs +++ b/src/line/quote.rs @@ -17,10 +17,6 @@ impl Quote { // Detect value let value = regex.get(1)?; - if value.trim().is_empty() { - return None; - } - // Result Some(Self { value: GString::from(value.as_str()), From c6f747fefd4f8a7f67f1519e911e2f8f3cf58189 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 3 Dec 2024 17:57:25 +0200 Subject: [PATCH 033/105] trim quote, replace GString with String --- src/line/quote.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/line/quote.rs b/src/line/quote.rs index 9ec6d38..986095f 100644 --- a/src/line/quote.rs +++ b/src/line/quote.rs @@ -1,7 +1,7 @@ -use glib::{GString, Regex, RegexCompileFlags, RegexMatchFlags}; +use glib::{Regex, RegexCompileFlags, RegexMatchFlags}; pub struct Quote { - pub value: GString, + pub value: String, } impl Quote { @@ -15,11 +15,11 @@ impl Quote { ); // Detect value - let value = regex.get(1)?; + let value = regex.get(1)?.trim(); // Result Some(Self { - value: GString::from(value.as_str()), + value: String::from(value), }) } } From 4741365154f7647b1570b44da8f9e02ac788644d Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 3 Dec 2024 18:00:29 +0200 Subject: [PATCH 034/105] trim list line, replace GString with String, allow empty value --- src/line/list.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/line/list.rs b/src/line/list.rs index 2aa1320..4c4d93f 100644 --- a/src/line/list.rs +++ b/src/line/list.rs @@ -1,7 +1,7 @@ -use glib::{GString, Regex, RegexCompileFlags, RegexMatchFlags}; +use glib::{Regex, RegexCompileFlags, RegexMatchFlags}; pub struct List { - pub value: GString, + pub value: String, } impl List { @@ -15,15 +15,11 @@ impl List { ); // Detect value - let value = regex.get(1)?; - - if value.trim().is_empty() { - return None; - } + let value = regex.get(1)?.trim(); // Result Some(Self { - value: GString::from(value.as_str()), + value: String::from(value), }) } } From 2e0a1ae3efc3e0cf6281f88d23bbd1309a567c51 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 3 Dec 2024 18:33:26 +0200 Subject: [PATCH 035/105] update value preparation, add comments --- src/line/list.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/line/list.rs b/src/line/list.rs index 4c4d93f..39f6c82 100644 --- a/src/line/list.rs +++ b/src/line/list.rs @@ -1,10 +1,12 @@ use glib::{Regex, RegexCompileFlags, RegexMatchFlags}; +/// [List item](https://geminiprotocol.net/docs/gemtext-specification.gmi#list-items) pub struct List { pub value: String, } impl List { + /// Parse `Self` from string pub fn from(line: &str) -> Option { // Parse line let regex = Regex::split_simple( @@ -14,12 +16,10 @@ impl List { RegexMatchFlags::DEFAULT, ); - // Detect value - let value = regex.get(1)?.trim(); + // Extract formatted value + let value = regex.get(1)?.trim().to_string(); // Result - Some(Self { - value: String::from(value), - }) + Some(Self { value }) } } From 155247a2ea498189283e24b42ae7f6e74a0c1fed Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 3 Dec 2024 18:43:34 +0200 Subject: [PATCH 036/105] add comments --- src/line/code.rs | 7 +++++-- src/line/code/inline.rs | 4 ++++ src/line/code/multiline.rs | 5 +++++ src/line/header.rs | 5 +++++ src/line/link.rs | 4 ++++ src/line/list.rs | 4 +++- src/line/quote.rs | 4 ++++ 7 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/line/code.rs b/src/line/code.rs index eb0c4df..d213e00 100644 --- a/src/line/code.rs +++ b/src/line/code.rs @@ -11,16 +11,19 @@ pub struct Code { } impl Code { - // Inline + // Constructors + + /// Parse inline `Self` from string pub fn inline_from(line: &str) -> Option { Inline::from(line) } - // Multiline + /// Begin multi-line parse `Self` from string pub fn multiline_begin_from(line: &str) -> Option { Multiline::begin_from(line) } + /// Continue multi-line parse `Self` from string pub fn multiline_continue_from(this: &mut Multiline, line: &str) -> Result<(), Error> { match Multiline::continue_from(this, line) { Ok(()) => Ok(()), diff --git a/src/line/code/inline.rs b/src/line/code/inline.rs index e52d03d..7607595 100644 --- a/src/line/code/inline.rs +++ b/src/line/code/inline.rs @@ -1,10 +1,14 @@ use glib::{Regex, RegexCompileFlags, RegexMatchFlags}; +/// Inline [preformatted](https://geminiprotocol.net/docs/gemtext-specification.gmi#in-pre-formatted-mode) entity holder pub struct Inline { pub value: String, } impl Inline { + // Constructors + + /// Parse `Self` from string pub fn from(line: &str) -> Option { // Parse line let regex = Regex::split_simple( diff --git a/src/line/code/multiline.rs b/src/line/code/multiline.rs index 06ab1de..4275d5b 100644 --- a/src/line/code/multiline.rs +++ b/src/line/code/multiline.rs @@ -1,9 +1,12 @@ pub mod error; pub use error::Error; +// Shared defaults + pub const NEW_LINE: char = '\n'; pub const TAG: &str = "```"; +/// Multi-line [preformatted](https://geminiprotocol.net/docs/gemtext-specification.gmi#in-pre-formatted-mode) entity holder pub struct Multiline { pub alt: Option, pub value: String, @@ -11,6 +14,8 @@ pub struct Multiline { } impl Multiline { + // Constructors + /// Search in line for tag open, /// return Self constructed on success or None pub fn begin_from(line: &str) -> Option { diff --git a/src/line/header.rs b/src/line/header.rs index 35cfab4..16c920e 100644 --- a/src/line/header.rs +++ b/src/line/header.rs @@ -1,17 +1,22 @@ use glib::{GString, Regex, RegexCompileFlags, RegexMatchFlags}; +/// [Header](https://geminiprotocol.net/docs/gemtext-specification.gmi#heading-lines) type holder pub enum Level { H1, H2, H3, } +/// [Header](https://geminiprotocol.net/docs/gemtext-specification.gmi#heading-lines) entity holder pub struct Header { pub value: GString, pub level: Level, } impl Header { + // Constructors + + /// Parse `Self` from string pub fn from(line: &str) -> Option { // Parse line let regex = Regex::split_simple( diff --git a/src/line/link.rs b/src/line/link.rs index 99f4047..fdd3cd1 100644 --- a/src/line/link.rs +++ b/src/line/link.rs @@ -1,5 +1,6 @@ use glib::{DateTime, Regex, RegexCompileFlags, RegexMatchFlags, TimeZone, Uri, UriFlags}; +/// [Link](https://geminiprotocol.net/docs/gemtext-specification.gmi#link-lines) entity holder pub struct Link { pub alt: Option, // [optional] alternative link description pub is_external: Option, // [optional] external link indication, on base option provided @@ -8,6 +9,9 @@ pub struct Link { } impl Link { + // Constructors + + /// Parse `Self` from string pub fn from(line: &str, base: Option<&Uri>, timezone: Option<&TimeZone>) -> Option { // Define initial values let mut alt = None; diff --git a/src/line/list.rs b/src/line/list.rs index 39f6c82..e2fc029 100644 --- a/src/line/list.rs +++ b/src/line/list.rs @@ -1,11 +1,13 @@ use glib::{Regex, RegexCompileFlags, RegexMatchFlags}; -/// [List item](https://geminiprotocol.net/docs/gemtext-specification.gmi#list-items) +/// [List](https://geminiprotocol.net/docs/gemtext-specification.gmi#list-items) entity holder pub struct List { pub value: String, } impl List { + // Constructors + /// Parse `Self` from string pub fn from(line: &str) -> Option { // Parse line diff --git a/src/line/quote.rs b/src/line/quote.rs index 986095f..c85cdd1 100644 --- a/src/line/quote.rs +++ b/src/line/quote.rs @@ -1,10 +1,14 @@ use glib::{Regex, RegexCompileFlags, RegexMatchFlags}; +/// [Quote](https://geminiprotocol.net/docs/gemtext-specification.gmi#quote-lines) entity holder pub struct Quote { pub value: String, } impl Quote { + // Constructors + + /// Parse `Self` from string pub fn from(line: &str) -> Option { // Parse line let regex = Regex::split_simple( From 3b9c1b1b1c8ee92b5d865158f975735f28e5c9ef Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 3 Dec 2024 18:45:54 +0200 Subject: [PATCH 037/105] replace GString with String --- src/line/header.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/line/header.rs b/src/line/header.rs index 16c920e..1bf3e72 100644 --- a/src/line/header.rs +++ b/src/line/header.rs @@ -1,4 +1,4 @@ -use glib::{GString, Regex, RegexCompileFlags, RegexMatchFlags}; +use glib::{Regex, RegexCompileFlags, RegexMatchFlags}; /// [Header](https://geminiprotocol.net/docs/gemtext-specification.gmi#heading-lines) type holder pub enum Level { @@ -9,7 +9,7 @@ pub enum Level { /// [Header](https://geminiprotocol.net/docs/gemtext-specification.gmi#heading-lines) entity holder pub struct Header { - pub value: GString, + pub value: String, pub level: Level, } @@ -37,16 +37,13 @@ impl Header { }; // Detect header value - let value = regex.get(2)?; + let value = regex.get(2)?.to_string(); if value.trim().is_empty() { return None; } // Result - Some(Self { - level, - value: GString::from(value.as_str()), - }) + Some(Self { level, value }) } } From d739181a768f7bee159dfd6556b64783034361a4 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 3 Dec 2024 18:47:03 +0200 Subject: [PATCH 038/105] use to_string method --- src/line/code/multiline.rs | 2 +- src/line/quote.rs | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/line/code/multiline.rs b/src/line/code/multiline.rs index 4275d5b..2662a94 100644 --- a/src/line/code/multiline.rs +++ b/src/line/code/multiline.rs @@ -25,7 +25,7 @@ impl Multiline { return Some(Self { alt: match alt.is_empty() { true => None, - false => Some(String::from(alt)), + false => Some(alt.to_string()), }, value: String::new(), completed: false, diff --git a/src/line/quote.rs b/src/line/quote.rs index c85cdd1..9434305 100644 --- a/src/line/quote.rs +++ b/src/line/quote.rs @@ -19,11 +19,9 @@ impl Quote { ); // Detect value - let value = regex.get(1)?.trim(); + let value = regex.get(1)?.trim().to_string(); // Result - Some(Self { - value: String::from(value), - }) + Some(Self { value }) } } From 84c72ae3a317b5d926675297ea9b69f0bc829c41 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 3 Dec 2024 18:55:44 +0200 Subject: [PATCH 039/105] remove middle-level code api, update comments --- src/line/code.rs | 32 ++------------------------------ src/line/code/error.rs | 16 ---------------- src/line/code/inline.rs | 2 +- src/line/code/multiline.rs | 4 ++-- src/line/header.rs | 2 +- src/line/link.rs | 2 +- src/line/list.rs | 2 +- src/line/quote.rs | 2 +- tests/integration.rs | 8 ++++---- 9 files changed, 13 insertions(+), 57 deletions(-) delete mode 100644 src/line/code/error.rs diff --git a/src/line/code.rs b/src/line/code.rs index d213e00..4d5e21b 100644 --- a/src/line/code.rs +++ b/src/line/code.rs @@ -1,33 +1,5 @@ -pub mod error; pub mod inline; pub mod multiline; -pub use error::Error; -use inline::Inline; -use multiline::Multiline; - -pub struct Code { - // nothing yet.. -} - -impl Code { - // Constructors - - /// Parse inline `Self` from string - pub fn inline_from(line: &str) -> Option { - Inline::from(line) - } - - /// Begin multi-line parse `Self` from string - pub fn multiline_begin_from(line: &str) -> Option { - Multiline::begin_from(line) - } - - /// Continue multi-line parse `Self` from string - pub fn multiline_continue_from(this: &mut Multiline, line: &str) -> Result<(), Error> { - match Multiline::continue_from(this, line) { - Ok(()) => Ok(()), - Err(e) => Err(Error::Multiline(e)), - } - } -} +pub use inline::Inline; +pub use multiline::Multiline; diff --git a/src/line/code/error.rs b/src/line/code/error.rs deleted file mode 100644 index 410dfe3..0000000 --- a/src/line/code/error.rs +++ /dev/null @@ -1,16 +0,0 @@ -use std::fmt::{Display, Formatter, Result}; - -#[derive(Debug)] -pub enum Error { - Multiline(crate::line::code::multiline::Error), -} - -impl Display for Error { - fn fmt(&self, f: &mut Formatter) -> Result { - match self { - Self::Multiline(e) => { - write!(f, "Multiline error: {e}") - } - } - } -} diff --git a/src/line/code/inline.rs b/src/line/code/inline.rs index 7607595..9e94394 100644 --- a/src/line/code/inline.rs +++ b/src/line/code/inline.rs @@ -8,7 +8,7 @@ pub struct Inline { impl Inline { // Constructors - /// Parse `Self` from string + /// Parse `Self` from line string pub fn from(line: &str) -> Option { // Parse line let regex = Regex::split_simple( diff --git a/src/line/code/multiline.rs b/src/line/code/multiline.rs index 2662a94..16e95a6 100644 --- a/src/line/code/multiline.rs +++ b/src/line/code/multiline.rs @@ -16,7 +16,7 @@ pub struct Multiline { impl Multiline { // Constructors - /// Search in line for tag open, + /// Search in line string for tag open, /// return Self constructed on success or None pub fn begin_from(line: &str) -> Option { if line.starts_with(TAG) { @@ -35,7 +35,7 @@ impl Multiline { None } - /// Continue preformatted buffer from line, + /// Continue preformatted buffer from line string, /// set `completed` as True on close tag found pub fn continue_from(&mut self, line: &str) -> Result<(), Error> { // Make sure buffer not completed yet diff --git a/src/line/header.rs b/src/line/header.rs index 1bf3e72..2e8eb6a 100644 --- a/src/line/header.rs +++ b/src/line/header.rs @@ -16,7 +16,7 @@ pub struct Header { impl Header { // Constructors - /// Parse `Self` from string + /// Parse `Self` from line string pub fn from(line: &str) -> Option { // Parse line let regex = Regex::split_simple( diff --git a/src/line/link.rs b/src/line/link.rs index fdd3cd1..26c2726 100644 --- a/src/line/link.rs +++ b/src/line/link.rs @@ -11,7 +11,7 @@ pub struct Link { impl Link { // Constructors - /// Parse `Self` from string + /// Parse `Self` from line string pub fn from(line: &str, base: Option<&Uri>, timezone: Option<&TimeZone>) -> Option { // Define initial values let mut alt = None; diff --git a/src/line/list.rs b/src/line/list.rs index e2fc029..2a1f328 100644 --- a/src/line/list.rs +++ b/src/line/list.rs @@ -8,7 +8,7 @@ pub struct List { impl List { // Constructors - /// Parse `Self` from string + /// Parse `Self` from line string pub fn from(line: &str) -> Option { // Parse line let regex = Regex::split_simple( diff --git a/src/line/quote.rs b/src/line/quote.rs index 9434305..aeef786 100644 --- a/src/line/quote.rs +++ b/src/line/quote.rs @@ -8,7 +8,7 @@ pub struct Quote { impl Quote { // Constructors - /// Parse `Self` from string + /// Parse `Self` from line string pub fn from(line: &str) -> Option { // Parse line let regex = Regex::split_simple( diff --git a/tests/integration.rs b/tests/integration.rs index 5ba2cda..c1b88a7 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1,5 +1,5 @@ use ggemtext::line::{ - code::{inline::Inline, multiline::Multiline, Code}, + code::{inline::Inline, multiline::Multiline}, header::{Header, Level}, link::Link, list::List, @@ -36,7 +36,7 @@ fn gemtext() { // Parse document by line for line in gemtext.lines() { // Inline code - if let Some(result) = Code::inline_from(line) { + if let Some(result) = Inline::from(line) { code_inline.push(result); continue; } @@ -44,13 +44,13 @@ fn gemtext() { // Multiline code match code_multiline_buffer { None => { - if let Some(code) = Code::multiline_begin_from(line) { + if let Some(code) = Multiline::begin_from(line) { code_multiline_buffer = Some(code); continue; } } Some(ref mut result) => { - assert!(Code::multiline_continue_from(result, line).is_ok()); + assert!(Multiline::continue_from(result, line).is_ok()); if result.completed { code_multiline.push(code_multiline_buffer.take().unwrap()); code_multiline_buffer = None; From 544851bbb778c702c7928f69c279b42b2bf50c52 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 3 Dec 2024 18:56:57 +0200 Subject: [PATCH 040/105] remove extra comment --- src/line/link.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/line/link.rs b/src/line/link.rs index 26c2726..6c6ef51 100644 --- a/src/line/link.rs +++ b/src/line/link.rs @@ -78,9 +78,6 @@ impl Link { // 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, From 12091be50a12f9ab278b44bdb7092af68426138b Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 3 Dec 2024 18:58:04 +0200 Subject: [PATCH 041/105] update comments --- src/line/link.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/line/link.rs b/src/line/link.rs index 6c6ef51..696c673 100644 --- a/src/line/link.rs +++ b/src/line/link.rs @@ -68,7 +68,7 @@ impl Link { } // Base resolve not requested None => { - // Just try convert address to valid URI + // Try convert address to valid URI match Uri::parse(&unresolved_address, UriFlags::NONE) { Ok(unresolved_uri) => unresolved_uri, Err(_) => return None, From 04120b2c44072607a7ecd672bdf7efa1d4864700 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 3 Dec 2024 19:04:17 +0200 Subject: [PATCH 042/105] update comments, create new string on parsed only --- src/line/code/inline.rs | 6 +++--- src/line/header.rs | 9 ++++++--- src/line/quote.rs | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/line/code/inline.rs b/src/line/code/inline.rs index 9e94394..01ad077 100644 --- a/src/line/code/inline.rs +++ b/src/line/code/inline.rs @@ -18,10 +18,10 @@ impl Inline { RegexMatchFlags::DEFAULT, ); - // Detect value - let value = regex.get(1)?; + // Extract formatted value + let value = regex.get(1)?.trim(); - if value.trim().is_empty() { + if value.is_empty() { return None; } diff --git a/src/line/header.rs b/src/line/header.rs index 2e8eb6a..d2e19cd 100644 --- a/src/line/header.rs +++ b/src/line/header.rs @@ -37,13 +37,16 @@ impl Header { }; // Detect header value - let value = regex.get(2)?.to_string(); + let value = regex.get(2)?.trim(); - if value.trim().is_empty() { + if value.is_empty() { return None; } // Result - Some(Self { level, value }) + Some(Self { + level, + value: value.to_string(), + }) } } diff --git a/src/line/quote.rs b/src/line/quote.rs index aeef786..128193b 100644 --- a/src/line/quote.rs +++ b/src/line/quote.rs @@ -18,7 +18,7 @@ impl Quote { RegexMatchFlags::DEFAULT, ); - // Detect value + // Extract formatted value let value = regex.get(1)?.trim().to_string(); // Result From 2716cc4f0eccdc05d43a5f94dec823f460cd8820 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 3 Dec 2024 19:07:35 +0200 Subject: [PATCH 043/105] update code api --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 0abc77a..f86cb24 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Line parser, useful for [TextTag](https://docs.gtk.org/gtk4/class.TextTag.html) ``` rust use ggemtext::line::{ - code::Code, + code::{Inline, Multiline}, header::{Header, Level}, link::Link, list::List, @@ -43,7 +43,7 @@ for line in gemtext.lines() { ##### Inline ``` rust -match Code::inline_from("```inline```") { +match Inline::from("```inline```") { Some(inline) => assert_eq!(inline.value, "inline"), None => assert!(false), }; @@ -52,11 +52,11 @@ match Code::inline_from("```inline```") { ##### Multiline ``` rust -match Code::multiline_begin_from("```alt") { +match Multiline::begin_from("```alt") { Some(mut multiline) => { - assert!(Code::multiline_continue_from(&mut multiline, "line 1").is_ok()); - assert!(Code::multiline_continue_from(&mut multiline, "line 2").is_ok()); - assert!(Code::multiline_continue_from(&mut multiline, "```").is_ok()); // complete + assert!(Multiline::continue_from(&mut multiline, "line 1").is_ok()); + assert!(Multiline::continue_from(&mut multiline, "line 2").is_ok()); + assert!(Multiline::continue_from(&mut multiline, "```").is_ok()); // complete assert!(multiline.completed); assert_eq!(multiline.alt, Some("alt".into())); From 429502ce72530109d9f980b9f98f37318f6e8c9b Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 3 Dec 2024 19:07:52 +0200 Subject: [PATCH 044/105] enshort namespace --- tests/integration.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration.rs b/tests/integration.rs index c1b88a7..c9ece69 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1,5 +1,5 @@ use ggemtext::line::{ - code::{inline::Inline, multiline::Multiline}, + code::{Inline, Multiline}, header::{Header, Level}, link::Link, list::List, From facb90dfa95a80b2d3da88b5e175384803d279ca Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 3 Dec 2024 19:36:01 +0200 Subject: [PATCH 045/105] fix regex condition to allow empty value --- src/line/list.rs | 2 +- src/line/quote.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/line/list.rs b/src/line/list.rs index 2a1f328..9cfb721 100644 --- a/src/line/list.rs +++ b/src/line/list.rs @@ -12,7 +12,7 @@ impl List { pub fn from(line: &str) -> Option { // Parse line let regex = Regex::split_simple( - r"^\*\s*(.+)$", + r"^\*\s*(.*)$", line, RegexCompileFlags::DEFAULT, RegexMatchFlags::DEFAULT, diff --git a/src/line/quote.rs b/src/line/quote.rs index 128193b..39dae62 100644 --- a/src/line/quote.rs +++ b/src/line/quote.rs @@ -12,7 +12,7 @@ impl Quote { pub fn from(line: &str) -> Option { // Parse line let regex = Regex::split_simple( - r"^>\s*(.+)$", + r"^>\s*(.*)$", line, RegexCompileFlags::DEFAULT, RegexMatchFlags::DEFAULT, From f08ae77b7c16dcbb567e7ed6510b4fa8f306ca6d Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 3 Dec 2024 19:38:39 +0200 Subject: [PATCH 046/105] remove extra condition by regex change --- src/line/code/inline.rs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/line/code/inline.rs b/src/line/code/inline.rs index 01ad077..1148966 100644 --- a/src/line/code/inline.rs +++ b/src/line/code/inline.rs @@ -12,22 +12,15 @@ impl Inline { pub fn from(line: &str) -> Option { // Parse line let regex = Regex::split_simple( - r"^`{3}([^`]*)`{3}$", + r"^`{3}([^`]+)`{3}$", line, RegexCompileFlags::DEFAULT, RegexMatchFlags::DEFAULT, ); // Extract formatted value - let value = regex.get(1)?.trim(); - - if value.is_empty() { - return None; - } - - // Result Some(Self { - value: value.to_string(), + value: regex.get(1)?.trim().to_string(), }) } } From b4d9686db51d4a43167d2b0b01fd4d3efa6863ef Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 3 Dec 2024 19:42:13 +0200 Subject: [PATCH 047/105] remove extra conditions --- src/line/header.rs | 26 +++++++------------------- src/line/list.rs | 7 +++---- src/line/quote.rs | 7 +++---- 3 files changed, 13 insertions(+), 27 deletions(-) diff --git a/src/line/header.rs b/src/line/header.rs index d2e19cd..e764931 100644 --- a/src/line/header.rs +++ b/src/line/header.rs @@ -26,27 +26,15 @@ impl Header { 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)?.trim(); - - if value.is_empty() { - return None; - } - // Result Some(Self { - level, - value: value.to_string(), + level: match regex.get(1)?.len() { + 1 => Level::H1, + 2 => Level::H2, + 3 => Level::H3, + _ => return None, + }, + value: regex.get(2)?.trim().to_string(), }) } } diff --git a/src/line/list.rs b/src/line/list.rs index 9cfb721..d9959f2 100644 --- a/src/line/list.rs +++ b/src/line/list.rs @@ -19,9 +19,8 @@ impl List { ); // Extract formatted value - let value = regex.get(1)?.trim().to_string(); - - // Result - Some(Self { value }) + Some(Self { + value: regex.get(1)?.trim().to_string(), + }) } } diff --git a/src/line/quote.rs b/src/line/quote.rs index 39dae62..8926165 100644 --- a/src/line/quote.rs +++ b/src/line/quote.rs @@ -19,9 +19,8 @@ impl Quote { ); // Extract formatted value - let value = regex.get(1)?.trim().to_string(); - - // Result - Some(Self { value }) + Some(Self { + value: regex.get(1)?.trim().to_string(), + }) } } From eccf5da808a462b01e1c1b7dbe4698033048a602 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 4 Dec 2024 05:51:22 +0200 Subject: [PATCH 048/105] add rustfmt, clippy validation --- .github/workflows/rust.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 9fd45e0..e4002e2 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -16,6 +16,10 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Run rustfmt + run: cargo fmt --all -- --check + - name: Run clippy + run: cargo clippy --all-targets - name: Build run: cargo build --verbose - name: Run tests From 3e30b79d85ac9d2a137c253a551cfcd834f0daf5 Mon Sep 17 00:00:00 2001 From: yggverse Date: Thu, 5 Dec 2024 06:28:02 +0200 Subject: [PATCH 049/105] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 565701e..09c64b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemtext" -version = "0.3.0" +version = "0.3.1" edition = "2021" license = "MIT" readme = "README.md" From 8428841f95f2d1cb8e1fb0de3b9107ecf5c16604 Mon Sep 17 00:00:00 2001 From: yggverse Date: Thu, 5 Dec 2024 06:30:32 +0200 Subject: [PATCH 050/105] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 09c64b6..565701e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemtext" -version = "0.3.1" +version = "0.3.0" edition = "2021" license = "MIT" readme = "README.md" From a9fed67a711a5bb278996c9f536578000d7440ee Mon Sep 17 00:00:00 2001 From: yggverse Date: Thu, 5 Dec 2024 06:36:36 +0200 Subject: [PATCH 051/105] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 565701e..09c64b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemtext" -version = "0.3.0" +version = "0.3.1" edition = "2021" license = "MIT" readme = "README.md" From 8c5e806bdc06a9f8ec00bc53baa9711e280d4398 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 7 Dec 2024 22:51:20 +0200 Subject: [PATCH 052/105] validate warnings --- .github/workflows/rust.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index e4002e2..9f5f88e 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -8,6 +8,7 @@ on: env: CARGO_TERM_COLOR: always + RUSTFLAGS: -Dwarnings jobs: build: From 9f7b85b523d73f06d14e25974622f5dd7296f988 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 7 Dec 2024 23:08:41 +0200 Subject: [PATCH 053/105] fix clippy warnings --- README.md | 2 +- tests/integration.rs | 38 +++++++++++++++++++++----------------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index f86cb24..6d619a4 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ match Link::from( match link.timestamp { Some(timestamp) => { assert_eq!(timestamp.year(), 1965); - assert_eq!(timestamp.month(), 01); + assert_eq!(timestamp.month(), 1); assert_eq!(timestamp.day_of_month(), 19); } None => assert!(false), diff --git a/tests/integration.rs b/tests/integration.rs index c9ece69..f665b09 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -86,25 +86,31 @@ fn gemtext() { // Validate inline code assert_eq!(code_inline.len(), 1); - assert_eq!(code_inline.get(0).unwrap().value, "inline code"); + assert_eq!(code_inline.first().unwrap().value, "inline code"); // Validate multiline code assert_eq!(code_multiline.len(), 2); { - let item = code_multiline.get(0).unwrap(); + let item = code_multiline.first().unwrap(); assert_eq!(item.alt.clone().unwrap(), "alt text"); + assert_eq!(item.value.lines().count(), 2); - assert_eq!(item.value.lines().nth(0).unwrap(), "multi"); - assert_eq!(item.value.lines().nth(1).unwrap(), " preformatted line"); + + let mut lines = item.value.lines(); + assert_eq!(lines.next().unwrap(), "multi"); + assert_eq!(lines.next().unwrap(), " preformatted line"); } // #1 { let item = code_multiline.get(1).unwrap(); assert_eq!(item.alt.clone(), None); + assert_eq!(item.value.lines().count(), 2); - assert_eq!(item.value.lines().nth(0).unwrap(), "alt-less"); - assert_eq!(item.value.lines().nth(1).unwrap(), " preformatted line"); + + let mut lines = item.value.lines(); + assert_eq!(lines.next().unwrap(), "alt-less"); + assert_eq!(lines.next().unwrap(), " preformatted line"); } // #2 // Validate headers @@ -119,7 +125,7 @@ fn gemtext() { } // comparison helper { - let item = header.get(0).unwrap(); + let item = header.first().unwrap(); assert_eq!(to_i8(&item.level), to_i8(&Level::H1)); assert_eq!(item.value, "H1"); @@ -143,7 +149,7 @@ fn gemtext() { assert_eq!(link.len(), 6); { - let item = link.get(0).unwrap(); + let item = link.first().unwrap(); assert_eq!(item.alt, None); assert_eq!(item.timestamp, None); @@ -157,7 +163,7 @@ fn gemtext() { let timestamp = item.timestamp.clone().unwrap(); assert_eq!(timestamp.year(), 1965); - assert_eq!(timestamp.month(), 01); + assert_eq!(timestamp.month(), 1); assert_eq!(timestamp.day_of_month(), 19); assert_eq!(item.uri.to_str(), "gemini://geminiprotocol.net"); @@ -178,7 +184,7 @@ fn gemtext() { let timestamp = item.timestamp.clone().unwrap(); assert_eq!(timestamp.year(), 1965); - assert_eq!(timestamp.month(), 01); + assert_eq!(timestamp.month(), 1); assert_eq!(timestamp.day_of_month(), 19); assert_eq!(item.uri.to_str(), "gemini://geminiprotocol.net"); @@ -191,7 +197,7 @@ fn gemtext() { let timestamp = item.timestamp.clone().unwrap(); assert_eq!(timestamp.year(), 1965); - assert_eq!(timestamp.month(), 01); + assert_eq!(timestamp.month(), 1); assert_eq!(timestamp.day_of_month(), 19); assert_eq!( @@ -210,16 +216,14 @@ fn gemtext() { // Validate lists assert_eq!(list.len(), 2); - assert_eq!(list.get(0).unwrap().value, "Listing item 1"); - assert_eq!(list.get(1).unwrap().value, "Listing item 2"); + assert_eq!(list.first().unwrap().value, "Listing item 1"); + assert_eq!(list.last().unwrap().value, "Listing item 2"); // Validate quotes assert_eq!(quote.len(), 1); - assert_eq!(quote.get(0).unwrap().value, "quoted string"); + assert_eq!(quote.first().unwrap().value, "quoted string"); } // Could not load gemtext file - Err(_) => { - assert!(false); - } + Err(_) => panic!(), } } From 4ea42809d70b7e40e9a5526ea151ee69d2df11c2 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 20 Dec 2024 10:20:10 +0200 Subject: [PATCH 054/105] rename workflow --- .github/workflows/{rust.yml => build.yml} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename .github/workflows/{rust.yml => build.yml} (97%) diff --git a/.github/workflows/rust.yml b/.github/workflows/build.yml similarity index 97% rename from .github/workflows/rust.yml rename to .github/workflows/build.yml index 9f5f88e..85edd8b 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: Rust +name: Build on: push: From c4d92d8d1e8bde3f41af97b8f2a8d506464d916e Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 20 Dec 2024 10:20:21 +0200 Subject: [PATCH 055/105] add badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 6d619a4..12c22e5 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # ggemtext +![Build](https://github.com/YGGverse/ggemtext/actions/workflows/build.yml/badge.svg) + Glib-oriented [Gemtext](https://geminiprotocol.net/docs/gemtext.gmi) API ## Install From e1cb4f9b99aa69bef2d4f2211ac1ea6b657cbb55 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 21 Dec 2024 20:30:08 +0200 Subject: [PATCH 056/105] update readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 12c22e5..82d562f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # ggemtext ![Build](https://github.com/YGGverse/ggemtext/actions/workflows/build.yml/badge.svg) +[![Documentation](https://docs.rs/ggemtext/badge.svg)](https://docs.rs/ggemtext) +[![crates.io](https://img.shields.io/crates/v/ggemtext.svg)](https://crates.io/crates/ggemtext) Glib-oriented [Gemtext](https://geminiprotocol.net/docs/gemtext.gmi) API From f3dc550c2eedd2e914944e67d76835a26a94bbb1 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 15 Feb 2025 21:07:01 +0200 Subject: [PATCH 057/105] fix new lines skip condition --- src/line/code/multiline.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/line/code/multiline.rs b/src/line/code/multiline.rs index 16e95a6..a046445 100644 --- a/src/line/code/multiline.rs +++ b/src/line/code/multiline.rs @@ -43,19 +43,16 @@ impl Multiline { return Err(Error::Completed); } + // Append to value, trim close tag on exists + self.value.push_str(line.trim_end_matches(TAG)); + // Line contain close tag if line.ends_with(TAG) { self.completed = true; - } - - // Prepend new line before next lines only - if !self.value.is_empty() { + } else { self.value.push(NEW_LINE); } - // Append to value, trim close tag on exists - self.value.push_str(line.trim_end_matches(TAG)); - Ok(()) } } From 407c3e2e13af9f6a725ced6775efedc06743ddd4 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 15 Feb 2025 21:10:58 +0200 Subject: [PATCH 058/105] add funding info --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..ada8a24 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: https://yggverse.github.io/#donate \ No newline at end of file From f2fbced415a965c46a6887d780d29119dcafee78 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 15 Feb 2025 21:12:47 +0200 Subject: [PATCH 059/105] add missed dependencies --- .github/workflows/build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 85edd8b..ad7ed4d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,6 +19,10 @@ jobs: - uses: actions/checkout@v4 - name: Run rustfmt run: cargo fmt --all -- --check + - name: Update packages index + run: sudo apt update + - name: Install system packages + run: sudo apt install -y libglib2.0-dev - name: Run clippy run: cargo clippy --all-targets - name: Build From b37a5f9061f8eec1c238e69ac8c647af306735f0 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 16 Feb 2025 22:36:41 +0200 Subject: [PATCH 060/105] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 09c64b6..ff29b23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemtext" -version = "0.3.1" +version = "0.3.2" edition = "2021" license = "MIT" readme = "README.md" From 7c2051acaf9c51a38006335eafc42cee5545d85e Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 16 Feb 2025 22:40:25 +0200 Subject: [PATCH 061/105] use `u8` --- README.md | 2 +- tests/integration.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 82d562f..2002925 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ match Multiline::begin_from("```alt") { ``` rust match Header::from("# H1") { Some(h1) => { - assert_eq!(h1.level as i8, Level::H1 as i8); + assert_eq!(h1.level as u8, Level::H1 as u8); assert_eq!(h1.value, "H1"); } None => assert!(false), diff --git a/tests/integration.rs b/tests/integration.rs index f665b09..2a4759c 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -116,7 +116,7 @@ fn gemtext() { // Validate headers assert_eq!(header.len(), 3); - fn to_i8(level: &Level) -> i8 { + fn to_u8(level: &Level) -> u8 { match level { Level::H1 => 1, Level::H2 => 2, @@ -127,21 +127,21 @@ fn gemtext() { { let item = header.first().unwrap(); - assert_eq!(to_i8(&item.level), to_i8(&Level::H1)); + assert_eq!(to_u8(&item.level), to_u8(&Level::H1)); assert_eq!(item.value, "H1"); } // #1 { let item = header.get(1).unwrap(); - assert_eq!(to_i8(&item.level), to_i8(&Level::H2)); + assert_eq!(to_u8(&item.level), to_u8(&Level::H2)); assert_eq!(item.value, "H2"); } // #2 { let item = header.get(2).unwrap(); - assert_eq!(to_i8(&item.level), to_i8(&Level::H3)); + assert_eq!(to_u8(&item.level), to_u8(&Level::H3)); assert_eq!(item.value, "H3"); } // #3 From ffcf8f9627b277cf0e70473874c88b0fc0d0ba5d Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 14 Mar 2025 22:45:35 +0200 Subject: [PATCH 062/105] fix relative scheme resolve --- src/line/link.rs | 29 +++++++++++++++++------------ tests/integration.gmi | 2 ++ tests/integration.rs | 18 +++++++++++++++++- 3 files changed, 36 insertions(+), 13 deletions(-) diff --git a/src/line/link.rs b/src/line/link.rs index 696c673..e009470 100644 --- a/src/line/link.rs +++ b/src/line/link.rs @@ -29,17 +29,25 @@ impl Link { // Detect address required to continue let mut unresolved_address = regex.get(1)?.to_string(); - // Seems that [Uri resolver](https://docs.gtk.org/glib/type_func.Uri.resolve_relative.html) - // does not support [protocol-relative URI](https://datatracker.ietf.org/doc/html/rfc3986#section-4.2) - // resolve manually - if unresolved_address.starts_with("//:") { - let scheme = match base { - Some(base) => base.scheme(), + // Relative scheme patch + // https://datatracker.ietf.org/doc/html/rfc3986#section-4.2 + if let Some(p) = unresolved_address.strip_prefix("//") { + let postfix = p.trim_start_matches(":"); + match base { + Some(b) => { + unresolved_address = format!( + "{}://{}", + b.scheme(), + if postfix.is_empty() { + b.host()? + } else { + postfix.into() + } + ) + } None => return None, - }; - unresolved_address = unresolved_address.replace("//:", &format!("{scheme}://")); + } } - // Convert address to the valid URI let uri = match base { // Base conversion requested @@ -54,10 +62,7 @@ impl Link { // 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, diff --git a/tests/integration.gmi b/tests/integration.gmi index 0a8bb64..cdb791d 100644 --- a/tests/integration.gmi +++ b/tests/integration.gmi @@ -8,6 +8,8 @@ => gemini://geminiprotocol.net 1965-01-19 Gemini => /docs/gemtext.gmi 1965-01-19 Gemini => //:geminiprotocol.net +=> //geminiprotocol.net +=> // * Listing item 1 * Listing item 2 diff --git a/tests/integration.rs b/tests/integration.rs index 2a4759c..e2cf653 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -146,7 +146,7 @@ fn gemtext() { } // #3 // Validate links - assert_eq!(link.len(), 6); + assert_eq!(link.len(), 8); { let item = link.first().unwrap(); @@ -214,6 +214,22 @@ fn gemtext() { assert_eq!(item.uri.to_str(), "gemini://geminiprotocol.net"); } // #6 + { + let item = link.get(6).unwrap(); + + assert_eq!(item.alt, None); + assert_eq!(item.timestamp, None); + assert_eq!(item.uri.to_str(), "gemini://geminiprotocol.net"); + } // #7 + + { + let item = link.get(7).unwrap(); + + assert_eq!(item.alt, None); + assert_eq!(item.timestamp, None); + assert_eq!(item.uri.to_str(), "gemini://geminiprotocol.net"); + } // #8 + // Validate lists assert_eq!(list.len(), 2); assert_eq!(list.first().unwrap().value, "Listing item 1"); From 094a404cf0f71e8ce6d95913e74e878ec50b7f35 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 14 Mar 2025 22:48:15 +0200 Subject: [PATCH 063/105] update base condition --- src/line/link.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/line/link.rs b/src/line/link.rs index e009470..4e967e6 100644 --- a/src/line/link.rs +++ b/src/line/link.rs @@ -38,7 +38,7 @@ impl Link { unresolved_address = format!( "{}://{}", b.scheme(), - if postfix.is_empty() { + if postfix.is_empty() || postfix == "/" { b.host()? } else { postfix.into() From 0364760a3547f31d58e594825a44c68465bb0b96 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 14 Mar 2025 22:57:05 +0200 Subject: [PATCH 064/105] update condition --- src/line/link.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/line/link.rs b/src/line/link.rs index 4e967e6..e009470 100644 --- a/src/line/link.rs +++ b/src/line/link.rs @@ -38,7 +38,7 @@ impl Link { unresolved_address = format!( "{}://{}", b.scheme(), - if postfix.is_empty() || postfix == "/" { + if postfix.is_empty() { b.host()? } else { postfix.into() From 2870aeb3fbd97a732e4daaaabd66f741bfc820b7 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 15 Mar 2025 13:55:27 +0200 Subject: [PATCH 065/105] add ending slash, reorganize conditions --- src/line/link.rs | 22 +++++++++------------- tests/integration.rs | 2 +- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/line/link.rs b/src/line/link.rs index e009470..9dfb6c5 100644 --- a/src/line/link.rs +++ b/src/line/link.rs @@ -32,21 +32,17 @@ impl Link { // Relative scheme patch // https://datatracker.ietf.org/doc/html/rfc3986#section-4.2 if let Some(p) = unresolved_address.strip_prefix("//") { + let b = base?; let postfix = p.trim_start_matches(":"); - match base { - Some(b) => { - unresolved_address = format!( - "{}://{}", - b.scheme(), - if postfix.is_empty() { - b.host()? - } else { - postfix.into() - } - ) + unresolved_address = format!( + "{}://{}", + b.scheme(), + if postfix.is_empty() { + format!("{}/", b.host()?) + } else { + postfix.into() } - None => return None, - } + ) } // Convert address to the valid URI let uri = match base { diff --git a/tests/integration.rs b/tests/integration.rs index e2cf653..cb62878 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -227,7 +227,7 @@ fn gemtext() { assert_eq!(item.alt, None); assert_eq!(item.timestamp, None); - assert_eq!(item.uri.to_str(), "gemini://geminiprotocol.net"); + assert_eq!(item.uri.to_str(), "gemini://geminiprotocol.net/"); } // #8 // Validate lists From 638d3cff0804ab9b336c88a3480fbde127337d48 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 15 Mar 2025 13:56:40 +0200 Subject: [PATCH 066/105] remove `is_external` detection from crate level --- src/line/link.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/line/link.rs b/src/line/link.rs index 9dfb6c5..29bbab5 100644 --- a/src/line/link.rs +++ b/src/line/link.rs @@ -3,7 +3,6 @@ use glib::{DateTime, Regex, RegexCompileFlags, RegexMatchFlags, TimeZone, Uri, U /// [Link](https://geminiprotocol.net/docs/gemtext-specification.gmi#link-lines) entity holder 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 } @@ -16,7 +15,6 @@ impl Link { // Define initial values let mut alt = None; let mut timestamp = None; - let mut is_external = None; // Begin line parse let regex = Regex::split_simple( @@ -57,10 +55,7 @@ impl Link { Ok(resolved_str) => { // Try convert string to the valid URI match Uri::parse(&resolved_str, UriFlags::NONE) { - Ok(resolved_uri) => { - is_external = Some(resolved_uri.scheme() != base_uri.scheme()); - resolved_uri - } + Ok(resolved_uri) => resolved_uri, Err(_) => return None, } } @@ -94,7 +89,6 @@ impl Link { Some(Self { alt, - is_external, timestamp, uri, }) From 4d2c05c428faede996835c37c95fe4fdaa22f390 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 15 Mar 2025 13:57:14 +0200 Subject: [PATCH 067/105] update minor version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index ff29b23..4940eef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemtext" -version = "0.3.2" +version = "0.4.0" edition = "2021" license = "MIT" readme = "README.md" From 085ec164b80a498e941c147bfc2e49c3c17b7b79 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 15 Mar 2025 14:44:10 +0200 Subject: [PATCH 068/105] update dependencies version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 4940eef..3786d04 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,5 +17,5 @@ repository = "https://github.com/YGGverse/ggemtext" [dependencies.glib] package = "glib" -version = "0.20.4" +version = "0.20.9" features = ["v2_66"] From bafdcda7be82c723ed346df6a5d0906fad4fbb62 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 15 Mar 2025 14:57:37 +0200 Subject: [PATCH 069/105] update tests --- tests/integration.gmi | 1 + tests/integration.rs | 56 +++++++++++++++++++++---------------------- 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/tests/integration.gmi b/tests/integration.gmi index cdb791d..a33000f 100644 --- a/tests/integration.gmi +++ b/tests/integration.gmi @@ -9,6 +9,7 @@ => /docs/gemtext.gmi 1965-01-19 Gemini => //:geminiprotocol.net => //geminiprotocol.net +=> //geminiprotocol.net/path => // * Listing item 1 diff --git a/tests/integration.rs b/tests/integration.rs index cb62878..6c28f35 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -16,8 +16,8 @@ fn gemtext() { // Init tags collection let mut code_inline: Vec = Vec::new(); let mut code_multiline: Vec = Vec::new(); - let mut header: Vec
= Vec::new(); - let mut link: Vec = Vec::new(); + let mut headers: Vec
= Vec::new(); + let mut links: Vec = Vec::new(); let mut list: Vec = Vec::new(); let mut quote: Vec = Vec::new(); @@ -61,13 +61,13 @@ fn gemtext() { // Header if let Some(result) = Header::from(line) { - header.push(result); + headers.push(result); continue; } // Link if let Some(result) = Link::from(line, base.as_ref(), timezone.as_ref()) { - link.push(result); + links.push(result); continue; } @@ -114,7 +114,7 @@ fn gemtext() { } // #2 // Validate headers - assert_eq!(header.len(), 3); + assert_eq!(headers.len(), 3); fn to_u8(level: &Level) -> u8 { match level { @@ -123,41 +123,38 @@ fn gemtext() { Level::H3 => 3, } } // comparison helper - + let mut header = headers.iter(); { - let item = header.first().unwrap(); + let item = header.next().unwrap(); assert_eq!(to_u8(&item.level), to_u8(&Level::H1)); assert_eq!(item.value, "H1"); } // #1 - { - let item = header.get(1).unwrap(); + let item = header.next().unwrap(); assert_eq!(to_u8(&item.level), to_u8(&Level::H2)); assert_eq!(item.value, "H2"); } // #2 - { - let item = header.get(2).unwrap(); + let item = header.next().unwrap(); assert_eq!(to_u8(&item.level), to_u8(&Level::H3)); assert_eq!(item.value, "H3"); } // #3 // Validate links - assert_eq!(link.len(), 8); - + assert_eq!(links.len(), 9); + let mut link = links.iter(); { - let item = link.first().unwrap(); + let item = link.next().unwrap(); assert_eq!(item.alt, None); assert_eq!(item.timestamp, None); assert_eq!(item.uri.to_str(), "gemini://geminiprotocol.net"); } // #1 - { - let item = link.get(1).unwrap(); + let item = link.next().unwrap(); assert_eq!(item.alt, None); @@ -168,17 +165,15 @@ fn gemtext() { assert_eq!(item.uri.to_str(), "gemini://geminiprotocol.net"); } // #2 - { - let item = link.get(2).unwrap(); + let item = link.next().unwrap(); assert_eq!(item.alt.clone().unwrap(), "Gemini"); assert_eq!(item.timestamp, None); assert_eq!(item.uri.to_str(), "gemini://geminiprotocol.net"); } // #3 - { - let item = link.get(3).unwrap(); + let item = link.next().unwrap(); assert_eq!(item.alt.clone().unwrap(), "Gemini"); @@ -189,9 +184,8 @@ fn gemtext() { assert_eq!(item.uri.to_str(), "gemini://geminiprotocol.net"); } // #4 - { - let item = link.get(4).unwrap(); + let item = link.next().unwrap(); assert_eq!(item.alt.clone().unwrap(), "Gemini"); @@ -205,30 +199,34 @@ fn gemtext() { "gemini://geminiprotocol.net/docs/gemtext.gmi" ); } // #5 - { - let item = link.get(5).unwrap(); + let item = link.next().unwrap(); assert_eq!(item.alt, None); assert_eq!(item.timestamp, None); assert_eq!(item.uri.to_str(), "gemini://geminiprotocol.net"); } // #6 - { - let item = link.get(6).unwrap(); + let item = link.next().unwrap(); assert_eq!(item.alt, None); assert_eq!(item.timestamp, None); assert_eq!(item.uri.to_str(), "gemini://geminiprotocol.net"); } // #7 - { - let item = link.get(7).unwrap(); + let item = link.next().unwrap(); + + assert_eq!(item.alt, None); + assert_eq!(item.timestamp, None); + assert_eq!(item.uri.to_str(), "gemini://geminiprotocol.net/path"); + } // #8 + { + let item = link.next().unwrap(); assert_eq!(item.alt, None); assert_eq!(item.timestamp, None); assert_eq!(item.uri.to_str(), "gemini://geminiprotocol.net/"); - } // #8 + } // #9 // Validate lists assert_eq!(list.len(), 2); From 7826104978a840473b82dbf6054de166ba5b72bb Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 15 Mar 2025 17:08:51 +0200 Subject: [PATCH 070/105] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 3786d04..7b7c959 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemtext" -version = "0.4.0" +version = "0.4.1" edition = "2021" license = "MIT" readme = "README.md" From bab4e0394022cf42414646802dfcd476ad57313e Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 16 Mar 2025 13:12:59 +0200 Subject: [PATCH 071/105] define child namespaces --- src/line.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/line.rs b/src/line.rs index fd70850..3b1a1fb 100644 --- a/src/line.rs +++ b/src/line.rs @@ -3,3 +3,8 @@ pub mod header; pub mod link; pub mod list; pub mod quote; + +pub use header::Header; +pub use link::Link; +pub use list::List; +pub use quote::Quote; From 4ce1b20bf7282cf5dc38ec458a0326656e772633 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 16 Mar 2025 13:42:23 +0200 Subject: [PATCH 072/105] rename constructor, implement zero-copy trait, remove extra regex parser --- src/line/list.rs | 54 +++++++++++++++++++++++++++++++++----------- tests/integration.rs | 2 +- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/src/line/list.rs b/src/line/list.rs index d9959f2..7d22f9b 100644 --- a/src/line/list.rs +++ b/src/line/list.rs @@ -1,4 +1,5 @@ -use glib::{Regex, RegexCompileFlags, RegexMatchFlags}; +/// [List item](https://geminiprotocol.net/docs/gemtext-specification.gmi#list-items) tag +pub const TAG: char = '*'; /// [List](https://geminiprotocol.net/docs/gemtext-specification.gmi#list-items) entity holder pub struct List { @@ -8,19 +9,46 @@ pub struct List { impl List { // Constructors - /// Parse `Self` from line string - pub fn from(line: &str) -> Option { - // Parse line - let regex = Regex::split_simple( - r"^\*\s*(.*)$", - line, - RegexCompileFlags::DEFAULT, - RegexMatchFlags::DEFAULT, - ); - - // Extract formatted value + /// Parse `Self` from [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) line + pub fn parse(line: &str) -> Option { Some(Self { - value: regex.get(1)?.trim().to_string(), + value: line.as_value()?.to_string(), }) } + + // Converters + + /// Convert `Self` to [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) line + pub fn as_source(&self) -> String { + self.value.to_source() + } +} + +pub trait Gemtext { + /// Get [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) value from `Self` + fn as_value(&self) -> Option<&Self>; + /// Convert `Self` to [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) line + fn to_source(&self) -> String; +} + +impl Gemtext for str { + fn as_value(&self) -> Option<&Self> { + self.strip_prefix(TAG).map(|s| s.trim()) + } + fn to_source(&self) -> String { + format!("{TAG} {}", self.trim()) + } +} + +#[test] +fn test() { + const SOURCE: &str = "* Item"; + const VALUE: &str = "Item"; + + // test struct + assert_eq!(List::parse(SOURCE).unwrap().value, VALUE); + + // test trait + assert_eq!(SOURCE.as_value(), Some(VALUE)); + assert_eq!(VALUE.to_source(), SOURCE) } diff --git a/tests/integration.rs b/tests/integration.rs index 6c28f35..2b4731b 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -72,7 +72,7 @@ fn gemtext() { } // List - if let Some(result) = List::from(line) { + if let Some(result) = List::parse(line) { list.push(result); continue; } From 23b04f26ec9f74e5014fddff6b5c24357fdc7a9d Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 16 Mar 2025 13:42:38 +0200 Subject: [PATCH 073/105] update examples --- README.md | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 2002925..6b9f9a9 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ use ggemtext::line::{ link::Link, list::List, quote::Quote, -}; +} ``` **Prepare document** @@ -50,7 +50,7 @@ for line in gemtext.lines() { match Inline::from("```inline```") { Some(inline) => assert_eq!(inline.value, "inline"), None => assert!(false), -}; +} ``` ##### Multiline @@ -67,7 +67,7 @@ match Multiline::begin_from("```alt") { assert_eq!(multiline.buffer.len(), 3); } None => assert!(false), -}; +} ``` #### Header @@ -79,7 +79,7 @@ match Header::from("# H1") { assert_eq!(h1.value, "H1"); } None => assert!(false), -}; // H1, H2, H3 +} // H1, H2, H3 ``` #### Link @@ -108,16 +108,26 @@ match Link::from( assert_eq!(link.uri.to_string(), "gemini://geminiprotocol.net"); } None => assert!(false), -}; +} ``` #### List +##### Struct + ``` rust -match List::from("* Item") { +match List::parse("* Item") { Some(list) => assert_eq!(list.value, "Item"), None => assert!(false), -}; +} +``` + +##### Trait + +``` rust +use ggemtext::line::list::Gemtext; +assert_eq!("* Item".as_value(), Some("Item")) +assert_eq!("Item".to_source(), "* Item") ``` #### Quote @@ -126,7 +136,7 @@ match List::from("* Item") { match Quote::from("> Quote") { Some(quote) => assert_eq!(quote.value, "Quote"), None => assert!(false), -}; +} ``` ## Integrations From 9696efa02d6c9baceefaee8c3b12749225240d60 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 16 Mar 2025 13:43:02 +0200 Subject: [PATCH 074/105] update versions --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7b7c959..72faff5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ggemtext" -version = "0.4.1" -edition = "2021" +version = "0.5.0" +edition = "2024" license = "MIT" readme = "README.md" description = "Glib-oriented Gemtext API" From 1b43f6aeafd0ae2f9343dc76ec4fb7c5aa2decb5 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 16 Mar 2025 13:47:00 +0200 Subject: [PATCH 075/105] fix method name, add missed test condition --- src/line/list.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/line/list.rs b/src/line/list.rs index 7d22f9b..b3ea5e9 100644 --- a/src/line/list.rs +++ b/src/line/list.rs @@ -19,7 +19,7 @@ impl List { // Converters /// Convert `Self` to [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) line - pub fn as_source(&self) -> String { + pub fn to_source(&self) -> String { self.value.to_source() } } @@ -45,10 +45,12 @@ fn test() { const SOURCE: &str = "* Item"; const VALUE: &str = "Item"; - // test struct - assert_eq!(List::parse(SOURCE).unwrap().value, VALUE); + // test `List` + let list = List::parse(SOURCE).unwrap(); + assert_eq!(list.value, VALUE); + assert_eq!(list.to_source(), SOURCE); - // test trait + // test `Gemtext` assert_eq!(SOURCE.as_value(), Some(VALUE)); assert_eq!(VALUE.to_source(), SOURCE) } From bf4ac4bd27149a58a112ebbbf7f9b6b1da7247f9 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 16 Mar 2025 14:43:08 +0200 Subject: [PATCH 076/105] rename constructor, implement zero-copy trait, remove extra regex parser --- src/line/quote.rs | 56 ++++++++++++++++++++++++++++++++++---------- tests/integration.rs | 2 +- 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/src/line/quote.rs b/src/line/quote.rs index 8926165..5ac1fb2 100644 --- a/src/line/quote.rs +++ b/src/line/quote.rs @@ -1,4 +1,5 @@ -use glib::{Regex, RegexCompileFlags, RegexMatchFlags}; +/// [Quote item](https://geminiprotocol.net/docs/gemtext-specification.gmi#quote-lines) tag +pub const TAG: char = '>'; /// [Quote](https://geminiprotocol.net/docs/gemtext-specification.gmi#quote-lines) entity holder pub struct Quote { @@ -8,19 +9,48 @@ pub struct Quote { impl Quote { // Constructors - /// Parse `Self` from line string - pub fn from(line: &str) -> Option { - // Parse line - let regex = Regex::split_simple( - r"^>\s*(.*)$", - line, - RegexCompileFlags::DEFAULT, - RegexMatchFlags::DEFAULT, - ); - - // Extract formatted value + /// Parse `Self` from [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) line + pub fn parse(line: &str) -> Option { Some(Self { - value: regex.get(1)?.trim().to_string(), + value: line.as_value()?.to_string(), }) } + + // Converters + + /// Convert `Self` to [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) line + pub fn to_source(&self) -> String { + self.value.to_source() + } +} + +pub trait Gemtext { + /// Get [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) value from `Self` + fn as_value(&self) -> Option<&Self>; + /// Convert `Self` to [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) line + fn to_source(&self) -> String; +} + +impl Gemtext for str { + fn as_value(&self) -> Option<&Self> { + self.strip_prefix(TAG).map(|s| s.trim()) + } + fn to_source(&self) -> String { + format!("{TAG} {}", self.trim()) + } +} + +#[test] +fn test() { + const SOURCE: &str = "> Quote"; + const VALUE: &str = "Quote"; + + // test `Quote` + let quote = Quote::parse(SOURCE).unwrap(); + assert_eq!(quote.value, VALUE); + assert_eq!(quote.to_source(), SOURCE); + + // test `Gemtext` + assert_eq!(SOURCE.as_value(), Some(VALUE)); + assert_eq!(VALUE.to_source(), SOURCE) } diff --git a/tests/integration.rs b/tests/integration.rs index 2b4731b..90c85dc 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -78,7 +78,7 @@ fn gemtext() { } // Quote - if let Some(result) = Quote::from(line) { + if let Some(result) = Quote::parse(line) { quote.push(result); continue; } From 9d27cdfb49718f686a2733ade7dbc30ceb809312 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 16 Mar 2025 14:45:13 +0200 Subject: [PATCH 077/105] update readme --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 6b9f9a9..abfef44 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,8 @@ assert_eq!("Item".to_source(), "* Item") #### Quote +##### Struct + ``` rust match Quote::from("> Quote") { Some(quote) => assert_eq!(quote.value, "Quote"), @@ -139,6 +141,14 @@ match Quote::from("> Quote") { } ``` +##### Trait + +``` rust +use ggemtext::line::quote::Gemtext; +assert_eq!("> Quote".as_value(), Some("Quote")) +assert_eq!("Quote".to_source(), "> Quote") +``` + ## Integrations * [Yoda](https://github.com/YGGverse/Yoda) - Browser for Gemini Protocol From 7802869d0db82aa83504a0bf76eafdc668460756 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 16 Mar 2025 15:43:27 +0200 Subject: [PATCH 078/105] remove regex dependency, rename constructor, implement Gemtext trait --- src/line/header.rs | 88 ++++++++++++++++++++++++++++++++++---------- tests/integration.rs | 2 +- 2 files changed, 70 insertions(+), 20 deletions(-) diff --git a/src/line/header.rs b/src/line/header.rs index e764931..061ce34 100644 --- a/src/line/header.rs +++ b/src/line/header.rs @@ -1,4 +1,8 @@ -use glib::{Regex, RegexCompileFlags, RegexMatchFlags}; +/// [Header](https://geminiprotocol.net/docs/gemtext-specification.gmi#heading-lines) tag +/// * store as entire static chars array +pub const TAG_H1: &str = "#"; +pub const TAG_H2: &str = "##"; +pub const TAG_H3: &str = "###"; /// [Header](https://geminiprotocol.net/docs/gemtext-specification.gmi#heading-lines) type holder pub enum Level { @@ -9,32 +13,78 @@ pub enum Level { /// [Header](https://geminiprotocol.net/docs/gemtext-specification.gmi#heading-lines) entity holder pub struct Header { - pub value: String, pub level: Level, + pub value: String, } impl Header { // Constructors /// Parse `Self` from line string - pub fn from(line: &str) -> Option { - // Parse line - let regex = Regex::split_simple( - r"^(#{1,3})\s*(.+)$", - line, - RegexCompileFlags::DEFAULT, - RegexMatchFlags::DEFAULT, - ); - - // Result + pub fn parse(line: &str) -> Option { Some(Self { - level: match regex.get(1)?.len() { - 1 => Level::H1, - 2 => Level::H2, - 3 => Level::H3, - _ => return None, - }, - value: regex.get(2)?.trim().to_string(), + level: line.to_level()?, + value: line.as_value()?.to_string(), }) } + + // Converters + + /// Convert `Self` to [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) line + pub fn to_source(&self) -> String { + self.value.to_source(&self.level) + } +} + +pub trait Gemtext { + /// Get [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) value from `Self` + fn as_value(&self) -> Option<&Self>; + /// Convert `Self` to `Level` + fn to_level(&self) -> Option; + /// Convert `Self` to [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) line + fn to_source(&self, level: &Level) -> String; +} + +impl Gemtext for str { + fn as_value(&self) -> Option<&Self> { + if let Some(h3) = self.strip_prefix(TAG_H3) { + if h3.trim_start().starts_with(TAG_H1) { + return None; // H4+ + } + return Some(h3.trim()); + } + if let Some(h2) = self.strip_prefix(TAG_H2) { + return Some(h2.trim()); + } + if let Some(h1) = self.strip_prefix(TAG_H1) { + return Some(h1.trim()); + } + None + } + fn to_level(&self) -> Option { + if let Some(h3) = self.strip_prefix(TAG_H3) { + if h3.trim_start().starts_with(TAG_H1) { + return None; // H4+ + } + return Some(Level::H3); + } + if self.starts_with(TAG_H2) { + return Some(Level::H2); + } + if self.starts_with(TAG_H1) { + return Some(Level::H1); + } + None + } + fn to_source(&self, level: &Level) -> String { + format!( + "{} {}", + match level { + Level::H1 => TAG_H1, + Level::H2 => TAG_H2, + Level::H3 => TAG_H3, + }, + self.trim() + ) + } } diff --git a/tests/integration.rs b/tests/integration.rs index 90c85dc..70924f6 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -60,7 +60,7 @@ fn gemtext() { }; // Header - if let Some(result) = Header::from(line) { + if let Some(result) = Header::parse(line) { headers.push(result); continue; } From 5b751e3c7ae2ac57063c9aed7443e369a18823a4 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 16 Mar 2025 15:53:24 +0200 Subject: [PATCH 079/105] change result data type to `&str` --- src/line/header.rs | 4 ++-- src/line/list.rs | 6 +++--- src/line/quote.rs | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/line/header.rs b/src/line/header.rs index 061ce34..c2f8647 100644 --- a/src/line/header.rs +++ b/src/line/header.rs @@ -37,7 +37,7 @@ impl Header { } pub trait Gemtext { - /// Get [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) value from `Self` + /// Get [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) value for `Self` fn as_value(&self) -> Option<&Self>; /// Convert `Self` to `Level` fn to_level(&self) -> Option; @@ -46,7 +46,7 @@ pub trait Gemtext { } impl Gemtext for str { - fn as_value(&self) -> Option<&Self> { + fn as_value(&self) -> Option<&str> { if let Some(h3) = self.strip_prefix(TAG_H3) { if h3.trim_start().starts_with(TAG_H1) { return None; // H4+ diff --git a/src/line/list.rs b/src/line/list.rs index b3ea5e9..0b6e966 100644 --- a/src/line/list.rs +++ b/src/line/list.rs @@ -25,14 +25,14 @@ impl List { } pub trait Gemtext { - /// Get [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) value from `Self` - fn as_value(&self) -> Option<&Self>; + /// Get [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) value for `Self` + fn as_value(&self) -> Option<&str>; /// Convert `Self` to [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) line fn to_source(&self) -> String; } impl Gemtext for str { - fn as_value(&self) -> Option<&Self> { + fn as_value(&self) -> Option<&str> { self.strip_prefix(TAG).map(|s| s.trim()) } fn to_source(&self) -> String { diff --git a/src/line/quote.rs b/src/line/quote.rs index 5ac1fb2..dbfb3fe 100644 --- a/src/line/quote.rs +++ b/src/line/quote.rs @@ -25,14 +25,14 @@ impl Quote { } pub trait Gemtext { - /// Get [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) value from `Self` - fn as_value(&self) -> Option<&Self>; + /// Get [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) value for `Self` + fn as_value(&self) -> Option<&str>; /// Convert `Self` to [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) line fn to_source(&self) -> String; } impl Gemtext for str { - fn as_value(&self) -> Option<&Self> { + fn as_value(&self) -> Option<&str> { self.strip_prefix(TAG).map(|s| s.trim()) } fn to_source(&self) -> String { From c29f1ba52950a2c9198e7b21e65ead2f65b13517 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 16 Mar 2025 16:38:38 +0200 Subject: [PATCH 080/105] separate traits --- src/line/list.rs | 24 +++--------------------- src/line/list/gemtext.rs | 26 ++++++++++++++++++++++++++ src/line/quote.rs | 25 ++++--------------------- src/line/quote/gemtext.rs | 26 ++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 42 deletions(-) create mode 100644 src/line/list/gemtext.rs create mode 100644 src/line/quote/gemtext.rs diff --git a/src/line/list.rs b/src/line/list.rs index 0b6e966..44d69f4 100644 --- a/src/line/list.rs +++ b/src/line/list.rs @@ -1,3 +1,6 @@ +pub mod gemtext; +pub use gemtext::Gemtext; + /// [List item](https://geminiprotocol.net/docs/gemtext-specification.gmi#list-items) tag pub const TAG: char = '*'; @@ -24,33 +27,12 @@ impl List { } } -pub trait Gemtext { - /// Get [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) value for `Self` - fn as_value(&self) -> Option<&str>; - /// Convert `Self` to [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) line - fn to_source(&self) -> String; -} - -impl Gemtext for str { - fn as_value(&self) -> Option<&str> { - self.strip_prefix(TAG).map(|s| s.trim()) - } - fn to_source(&self) -> String { - format!("{TAG} {}", self.trim()) - } -} - #[test] fn test() { const SOURCE: &str = "* Item"; const VALUE: &str = "Item"; - // test `List` let list = List::parse(SOURCE).unwrap(); assert_eq!(list.value, VALUE); assert_eq!(list.to_source(), SOURCE); - - // test `Gemtext` - assert_eq!(SOURCE.as_value(), Some(VALUE)); - assert_eq!(VALUE.to_source(), SOURCE) } diff --git a/src/line/list/gemtext.rs b/src/line/list/gemtext.rs new file mode 100644 index 0000000..5700287 --- /dev/null +++ b/src/line/list/gemtext.rs @@ -0,0 +1,26 @@ +use super::TAG; + +pub trait Gemtext { + /// Get [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) value for `Self` + fn as_value(&self) -> Option<&str>; + /// Convert `Self` to [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) line + fn to_source(&self) -> String; +} + +impl Gemtext for str { + fn as_value(&self) -> Option<&str> { + self.strip_prefix(TAG).map(|s| s.trim()) + } + fn to_source(&self) -> String { + format!("{TAG} {}", self.trim()) + } +} + +#[test] +fn test() { + const SOURCE: &str = "* Item"; + const VALUE: &str = "Item"; + + assert_eq!(SOURCE.as_value(), Some(VALUE)); + assert_eq!(VALUE.to_source(), SOURCE) +} diff --git a/src/line/quote.rs b/src/line/quote.rs index dbfb3fe..e098703 100644 --- a/src/line/quote.rs +++ b/src/line/quote.rs @@ -1,3 +1,6 @@ +pub mod gemtext; +pub use gemtext::Gemtext; + /// [Quote item](https://geminiprotocol.net/docs/gemtext-specification.gmi#quote-lines) tag pub const TAG: char = '>'; @@ -24,33 +27,13 @@ impl Quote { } } -pub trait Gemtext { - /// Get [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) value for `Self` - fn as_value(&self) -> Option<&str>; - /// Convert `Self` to [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) line - fn to_source(&self) -> String; -} - -impl Gemtext for str { - fn as_value(&self) -> Option<&str> { - self.strip_prefix(TAG).map(|s| s.trim()) - } - fn to_source(&self) -> String { - format!("{TAG} {}", self.trim()) - } -} - #[test] fn test() { const SOURCE: &str = "> Quote"; const VALUE: &str = "Quote"; - // test `Quote` let quote = Quote::parse(SOURCE).unwrap(); + assert_eq!(quote.value, VALUE); assert_eq!(quote.to_source(), SOURCE); - - // test `Gemtext` - assert_eq!(SOURCE.as_value(), Some(VALUE)); - assert_eq!(VALUE.to_source(), SOURCE) } diff --git a/src/line/quote/gemtext.rs b/src/line/quote/gemtext.rs new file mode 100644 index 0000000..e55d2f3 --- /dev/null +++ b/src/line/quote/gemtext.rs @@ -0,0 +1,26 @@ +use super::TAG; + +pub trait Gemtext { + /// Get [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) value for `Self` + fn as_value(&self) -> Option<&str>; + /// Convert `Self` to [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) line + fn to_source(&self) -> String; +} + +impl Gemtext for str { + fn as_value(&self) -> Option<&str> { + self.strip_prefix(TAG).map(|s| s.trim()) + } + fn to_source(&self) -> String { + format!("{TAG} {}", self.trim()) + } +} + +#[test] +fn test() { + const SOURCE: &str = "> Quote"; + const VALUE: &str = "Quote"; + + assert_eq!(SOURCE.as_value(), Some(VALUE)); + assert_eq!(VALUE.to_source(), SOURCE) +} From 3e16995e005a9b5eb168e3785123c634fa143df3 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 16 Mar 2025 16:39:02 +0200 Subject: [PATCH 081/105] reorganize header component --- src/line/header.rs | 91 ++++++++++---------------------------- src/line/header/gemtext.rs | 66 +++++++++++++++++++++++++++ src/line/header/level.rs | 16 +++++++ 3 files changed, 105 insertions(+), 68 deletions(-) create mode 100644 src/line/header/gemtext.rs create mode 100644 src/line/header/level.rs diff --git a/src/line/header.rs b/src/line/header.rs index c2f8647..7745a5d 100644 --- a/src/line/header.rs +++ b/src/line/header.rs @@ -1,15 +1,8 @@ -/// [Header](https://geminiprotocol.net/docs/gemtext-specification.gmi#heading-lines) tag -/// * store as entire static chars array -pub const TAG_H1: &str = "#"; -pub const TAG_H2: &str = "##"; -pub const TAG_H3: &str = "###"; +pub mod gemtext; +pub mod level; -/// [Header](https://geminiprotocol.net/docs/gemtext-specification.gmi#heading-lines) type holder -pub enum Level { - H1, - H2, - H3, -} +pub use gemtext::Gemtext; +pub use level::Level; /// [Header](https://geminiprotocol.net/docs/gemtext-specification.gmi#heading-lines) entity holder pub struct Header { @@ -22,10 +15,25 @@ impl Header { /// Parse `Self` from line string pub fn parse(line: &str) -> Option { - Some(Self { - level: line.to_level()?, - value: line.as_value()?.to_string(), - }) + if let Some(value) = line.as_h1_value() { + return Some(Self { + level: Level::H1, + value: value.to_string(), + }); + } + if let Some(value) = line.as_h2_value() { + return Some(Self { + level: Level::H2, + value: value.to_string(), + }); + } + if let Some(value) = line.as_h3_value() { + return Some(Self { + level: Level::H3, + value: value.to_string(), + }); + } + None } // Converters @@ -35,56 +43,3 @@ impl Header { self.value.to_source(&self.level) } } - -pub trait Gemtext { - /// Get [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) value for `Self` - fn as_value(&self) -> Option<&Self>; - /// Convert `Self` to `Level` - fn to_level(&self) -> Option; - /// Convert `Self` to [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) line - fn to_source(&self, level: &Level) -> String; -} - -impl Gemtext for str { - fn as_value(&self) -> Option<&str> { - if let Some(h3) = self.strip_prefix(TAG_H3) { - if h3.trim_start().starts_with(TAG_H1) { - return None; // H4+ - } - return Some(h3.trim()); - } - if let Some(h2) = self.strip_prefix(TAG_H2) { - return Some(h2.trim()); - } - if let Some(h1) = self.strip_prefix(TAG_H1) { - return Some(h1.trim()); - } - None - } - fn to_level(&self) -> Option { - if let Some(h3) = self.strip_prefix(TAG_H3) { - if h3.trim_start().starts_with(TAG_H1) { - return None; // H4+ - } - return Some(Level::H3); - } - if self.starts_with(TAG_H2) { - return Some(Level::H2); - } - if self.starts_with(TAG_H1) { - return Some(Level::H1); - } - None - } - fn to_source(&self, level: &Level) -> String { - format!( - "{} {}", - match level { - Level::H1 => TAG_H1, - Level::H2 => TAG_H2, - Level::H3 => TAG_H3, - }, - self.trim() - ) - } -} diff --git a/src/line/header/gemtext.rs b/src/line/header/gemtext.rs new file mode 100644 index 0000000..51e946a --- /dev/null +++ b/src/line/header/gemtext.rs @@ -0,0 +1,66 @@ +use super::Level; + +pub trait Gemtext { + /// Get [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) value for `Self` + fn as_value(&self) -> Option<&str>; + /// Get parsed H1 header value for `Self` + fn as_h1_value(&self) -> Option<&str>; + /// Get parsed H2 header value `Self` + fn as_h2_value(&self) -> Option<&str>; + /// Get parsed H3 header value `Self` + fn as_h3_value(&self) -> Option<&str>; + /// Get parsed header value `Self` match `Level` + fn as_value_match_level(&self, level: Level) -> Option<&str>; + /// Convert `Self` to `Level` + fn to_level(&self) -> Option; + /// Convert `Self` to [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) line + fn to_source(&self, level: &Level) -> String; +} + +impl Gemtext for str { + fn as_value(&self) -> Option<&str> { + if let Some(value) = self.as_h1_value() { + return Some(value); + } + if let Some(value) = self.as_h2_value() { + return Some(value); + } + if let Some(value) = self.as_h3_value() { + return Some(value); + } + None + } + fn as_h1_value(&self) -> Option<&str> { + self.as_value_match_level(Level::H1) + } + fn as_h2_value(&self) -> Option<&str> { + self.as_value_match_level(Level::H2) + } + fn as_h3_value(&self) -> Option<&str> { + self.as_value_match_level(Level::H3) + } + fn as_value_match_level(&self, level: Level) -> Option<&str> { + if let Some(value) = self.strip_prefix(level.as_tag()) { + if value.trim_start().starts_with(Level::H1.as_tag()) { + return None; + } + return Some(value.trim()); + } + None + } + fn to_level(&self) -> Option { + if self.as_h1_value().is_some() { + return Some(Level::H1); + } + if self.as_h2_value().is_some() { + return Some(Level::H2); + } + if self.as_h3_value().is_some() { + return Some(Level::H3); + } + None + } + fn to_source(&self, level: &Level) -> String { + format!("{} {}", level.as_tag(), self.trim()) + } +} diff --git a/src/line/header/level.rs b/src/line/header/level.rs new file mode 100644 index 0000000..2fa001b --- /dev/null +++ b/src/line/header/level.rs @@ -0,0 +1,16 @@ +/// [Header](https://geminiprotocol.net/docs/gemtext-specification.gmi#heading-lines) type holder +pub enum Level { + H1, + H2, + H3, +} + +impl Level { + pub fn as_tag(&self) -> &str { + match self { + Level::H1 => "#", + Level::H2 => "##", + Level::H3 => "###", + } + } +} From 9392b39327a364bd9b28ddd5d101b9d414f2e34b Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 16 Mar 2025 16:44:58 +0200 Subject: [PATCH 082/105] remove duplicated header anchors --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index abfef44..98ec55c 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ match Link::from( #### List -##### Struct +**Struct** ``` rust match List::parse("* Item") { @@ -122,7 +122,7 @@ match List::parse("* Item") { } ``` -##### Trait +**Trait** ``` rust use ggemtext::line::list::Gemtext; @@ -132,7 +132,7 @@ assert_eq!("Item".to_source(), "* Item") #### Quote -##### Struct +**Struct** ``` rust match Quote::from("> Quote") { @@ -141,7 +141,7 @@ match Quote::from("> Quote") { } ``` -##### Trait +**Trait** ``` rust use ggemtext::line::quote::Gemtext; From f550041b5512d78da621499dc7821e42d94a3c1b Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 16 Mar 2025 17:28:56 +0200 Subject: [PATCH 083/105] implement test --- src/line/header.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/line/header.rs b/src/line/header.rs index 7745a5d..e32ab71 100644 --- a/src/line/header.rs +++ b/src/line/header.rs @@ -43,3 +43,33 @@ impl Header { self.value.to_source(&self.level) } } + +#[test] +fn test() { + fn test(source: &str, value: &str) { + fn filter(s: &str) -> String { + s.chars().filter(|&c| c != ' ').collect() + } + let header = Header::parse(source).unwrap(); + assert_eq!(header.value, value); + assert_eq!(filter(&header.to_source()), filter(source)); + } + // h1 + test("# H1", "H1"); + test("# H1 ", "H1"); + test("#H1", "H1"); + test("#H1 ", "H1"); + // h2 + test("## H2", "H2"); + test("## H2 ", "H2"); + test("##H2", "H2"); + test("##H2 ", "H2"); + // h3 + test("### H3", "H3"); + test("### H3 ", "H3"); + test("###H3", "H3"); + test("###H3 ", "H3"); + // other + assert!(Header::parse("H").is_none()); + assert!(Header::parse("#### H").is_none()) +} From 039b1db9359815cc71ed178afcdfd96080ea4668 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 16 Mar 2025 17:37:20 +0200 Subject: [PATCH 084/105] test `Level` member --- src/line/header.rs | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/line/header.rs b/src/line/header.rs index e32ab71..1455963 100644 --- a/src/line/header.rs +++ b/src/line/header.rs @@ -46,29 +46,30 @@ impl Header { #[test] fn test() { - fn test(source: &str, value: &str) { - fn filter(s: &str) -> String { + fn test(source: &str, value: &str, level: Level) { + fn f(s: &str) -> String { s.chars().filter(|&c| c != ' ').collect() } let header = Header::parse(source).unwrap(); assert_eq!(header.value, value); - assert_eq!(filter(&header.to_source()), filter(source)); + assert_eq!(header.level.as_tag(), level.as_tag()); + assert_eq!(f(&header.to_source()), f(source)); } // h1 - test("# H1", "H1"); - test("# H1 ", "H1"); - test("#H1", "H1"); - test("#H1 ", "H1"); + test("# H1", "H1", Level::H1); + test("# H1 ", "H1", Level::H1); + test("#H1", "H1", Level::H1); + test("#H1 ", "H1", Level::H1); // h2 - test("## H2", "H2"); - test("## H2 ", "H2"); - test("##H2", "H2"); - test("##H2 ", "H2"); + test("## H2", "H2", Level::H2); + test("## H2 ", "H2", Level::H2); + test("##H2", "H2", Level::H2); + test("##H2 ", "H2", Level::H2); // h3 - test("### H3", "H3"); - test("### H3 ", "H3"); - test("###H3", "H3"); - test("###H3 ", "H3"); + test("### H3", "H3", Level::H3); + test("### H3 ", "H3", Level::H3); + test("###H3", "H3", Level::H3); + test("###H3 ", "H3", Level::H3); // other assert!(Header::parse("H").is_none()); assert!(Header::parse("#### H").is_none()) From eedd7a73ff504cf34a370c3214bccfe862de109a Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 16 Mar 2025 17:53:03 +0200 Subject: [PATCH 085/105] trim once --- src/line/header/gemtext.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/line/header/gemtext.rs b/src/line/header/gemtext.rs index 51e946a..ef792d8 100644 --- a/src/line/header/gemtext.rs +++ b/src/line/header/gemtext.rs @@ -40,11 +40,12 @@ impl Gemtext for str { self.as_value_match_level(Level::H3) } fn as_value_match_level(&self, level: Level) -> Option<&str> { - if let Some(value) = self.strip_prefix(level.as_tag()) { - if value.trim_start().starts_with(Level::H1.as_tag()) { + if let Some(postfix) = self.strip_prefix(level.as_tag()) { + let value = postfix.trim(); + if value.starts_with(Level::H1.as_tag()) { return None; } - return Some(value.trim()); + return Some(value); } None } From d72575fdc5cd05b83f1729d1a48a7c36a31c95bc Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 16 Mar 2025 17:55:38 +0200 Subject: [PATCH 086/105] simplify --- src/line/header/gemtext.rs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/line/header/gemtext.rs b/src/line/header/gemtext.rs index ef792d8..ebf3755 100644 --- a/src/line/header/gemtext.rs +++ b/src/line/header/gemtext.rs @@ -40,14 +40,9 @@ impl Gemtext for str { self.as_value_match_level(Level::H3) } fn as_value_match_level(&self, level: Level) -> Option<&str> { - if let Some(postfix) = self.strip_prefix(level.as_tag()) { - let value = postfix.trim(); - if value.starts_with(Level::H1.as_tag()) { - return None; - } - return Some(value); - } - None + self.strip_prefix(level.as_tag()) + .map(|postfix| postfix.trim()) + .filter(|value| !value.starts_with(Level::H1.as_tag())) } fn to_level(&self) -> Option { if self.as_h1_value().is_some() { From a5638fde33a88935c34650325b32d409cd67689c Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 16 Mar 2025 18:46:12 +0200 Subject: [PATCH 087/105] implement tests --- src/line/header/gemtext.rs | 39 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/line/header/gemtext.rs b/src/line/header/gemtext.rs index ebf3755..da0b68b 100644 --- a/src/line/header/gemtext.rs +++ b/src/line/header/gemtext.rs @@ -60,3 +60,42 @@ impl Gemtext for str { format!("{} {}", level.as_tag(), self.trim()) } } + +#[test] +fn test() { + const VALUE: &str = "H"; + let mut value: Option<&str> = Some(VALUE); + for t in ["#", "##", "###", "####"] { + if t.len() > 3 { + value = None; + } + assert_eq!(format!("{t}{VALUE}").as_value(), value); + assert_eq!(format!("{t}{VALUE} ").as_value(), value); + assert_eq!(format!("{t} {VALUE}").as_value(), value); + assert_eq!(format!("{t} {VALUE} ").as_value(), value); + } + + fn to_source(level: &Level) { + assert_eq!( + VALUE.to_source(level), + format!("{} {VALUE}", level.as_tag()) + ); + } + to_source(&Level::H1); + to_source(&Level::H2); + to_source(&Level::H3); + + fn to_level(l: &Level) { + fn assert(s: String, l: &str) { + assert_eq!(s.to_level().unwrap().as_tag(), l); + } + let t = l.as_tag(); + assert(format!("{t} {VALUE}"), t); + assert(format!("{t} {VALUE} "), t); + assert(format!("{t}{VALUE} "), t); + assert(format!("{t} {VALUE} "), t); + } + to_level(&Level::H1); + to_level(&Level::H2); + to_level(&Level::H3); +} From 8eaad1dac9ede80365f39887dfc1067b522f6fd3 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 16 Mar 2025 18:50:56 +0200 Subject: [PATCH 088/105] enshort var name --- src/line/header/gemtext.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/line/header/gemtext.rs b/src/line/header/gemtext.rs index da0b68b..283edbb 100644 --- a/src/line/header/gemtext.rs +++ b/src/line/header/gemtext.rs @@ -75,11 +75,8 @@ fn test() { assert_eq!(format!("{t} {VALUE} ").as_value(), value); } - fn to_source(level: &Level) { - assert_eq!( - VALUE.to_source(level), - format!("{} {VALUE}", level.as_tag()) - ); + fn to_source(l: &Level) { + assert_eq!(VALUE.to_source(l), format!("{} {VALUE}", l.as_tag())); } to_source(&Level::H1); to_source(&Level::H2); From 73454001725eec2950a31534652a930ad72fdaef Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 16 Mar 2025 19:43:15 +0200 Subject: [PATCH 089/105] update readme --- README.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 98ec55c..01ef74f 100644 --- a/README.md +++ b/README.md @@ -72,8 +72,10 @@ match Multiline::begin_from("```alt") { #### Header +**Struct** + ``` rust -match Header::from("# H1") { +match Header::parse("# H1") { Some(h1) => { assert_eq!(h1.level as u8, Level::H1 as u8); assert_eq!(h1.value, "H1"); @@ -82,6 +84,15 @@ match Header::from("# H1") { } // H1, H2, H3 ``` +**Trait** + +``` rust +use ggemtext::line::header::{Gemtext, Level}; +assert_eq!("# H1".as_value(), Some("H1")); +assert_eq!("H1".to_source(&Level::H1), "# H1"); +// H1, H2, H3 +``` + #### Link ``` rust @@ -135,7 +146,7 @@ assert_eq!("Item".to_source(), "* Item") **Struct** ``` rust -match Quote::from("> Quote") { +match Quote::parse("> Quote") { Some(quote) => assert_eq!(quote.value, "Quote"), None => assert!(false), } From 83ec663929f55ca0a2a03f10e900aac21db1e810 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 16 Mar 2025 19:53:29 +0200 Subject: [PATCH 090/105] skip regex operations on tag mismatch subject --- src/line/code.rs | 2 ++ src/line/code/inline.rs | 7 +++++++ src/line/code/multiline.rs | 3 ++- src/line/link.rs | 7 +++++++ 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/line/code.rs b/src/line/code.rs index 4d5e21b..60345bb 100644 --- a/src/line/code.rs +++ b/src/line/code.rs @@ -3,3 +3,5 @@ pub mod multiline; pub use inline::Inline; pub use multiline::Multiline; + +pub const TAG: &str = "```"; diff --git a/src/line/code/inline.rs b/src/line/code/inline.rs index 1148966..3569a29 100644 --- a/src/line/code/inline.rs +++ b/src/line/code/inline.rs @@ -1,3 +1,4 @@ +use super::TAG; use glib::{Regex, RegexCompileFlags, RegexMatchFlags}; /// Inline [preformatted](https://geminiprotocol.net/docs/gemtext-specification.gmi#in-pre-formatted-mode) entity holder @@ -10,6 +11,12 @@ impl Inline { /// Parse `Self` from line string pub fn from(line: &str) -> Option { + // Skip next operations on prefix and postfix mismatch `TAG` + // * replace regex implementation @TODO + if !line.starts_with(TAG) && !line.ends_with(TAG) { + return None; + } + // Parse line let regex = Regex::split_simple( r"^`{3}([^`]+)`{3}$", diff --git a/src/line/code/multiline.rs b/src/line/code/multiline.rs index a046445..2e1ef32 100644 --- a/src/line/code/multiline.rs +++ b/src/line/code/multiline.rs @@ -1,10 +1,11 @@ +use super::TAG; + pub mod error; pub use error::Error; // Shared defaults pub const NEW_LINE: char = '\n'; -pub const TAG: &str = "```"; /// Multi-line [preformatted](https://geminiprotocol.net/docs/gemtext-specification.gmi#in-pre-formatted-mode) entity holder pub struct Multiline { diff --git a/src/line/link.rs b/src/line/link.rs index 29bbab5..b193032 100644 --- a/src/line/link.rs +++ b/src/line/link.rs @@ -1,5 +1,7 @@ use glib::{DateTime, Regex, RegexCompileFlags, RegexMatchFlags, TimeZone, Uri, UriFlags}; +pub const TAG: &str = "=>"; + /// [Link](https://geminiprotocol.net/docs/gemtext-specification.gmi#link-lines) entity holder pub struct Link { pub alt: Option, // [optional] alternative link description @@ -12,6 +14,11 @@ impl Link { /// Parse `Self` from line string pub fn from(line: &str, base: Option<&Uri>, timezone: Option<&TimeZone>) -> Option { + // Skip next operations on prefix mismatch + // * replace regex implementation @TODO + if !line.starts_with(TAG) { + return None; + } // Define initial values let mut alt = None; let mut timestamp = None; From 7f3ea670f1236e38eb0c9055d1942c86211a6b5f Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 16 Mar 2025 19:54:36 +0200 Subject: [PATCH 091/105] add new line separator --- src/line/link.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/line/link.rs b/src/line/link.rs index b193032..4b7d4bc 100644 --- a/src/line/link.rs +++ b/src/line/link.rs @@ -19,6 +19,7 @@ impl Link { if !line.starts_with(TAG) { return None; } + // Define initial values let mut alt = None; let mut timestamp = None; From 96f7d648b601cee5c3f5917b3c3249e1404c2d3b Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 17 Mar 2025 02:09:52 +0200 Subject: [PATCH 092/105] remove regex dependency, rename constructor, implement zero-copy trait --- src/line/code/inline.rs | 51 +++++++++++++++++++++------------ src/line/code/inline/gemtext.rs | 38 ++++++++++++++++++++++++ tests/integration.rs | 2 +- 3 files changed, 71 insertions(+), 20 deletions(-) create mode 100644 src/line/code/inline/gemtext.rs diff --git a/src/line/code/inline.rs b/src/line/code/inline.rs index 3569a29..a3cbc01 100644 --- a/src/line/code/inline.rs +++ b/src/line/code/inline.rs @@ -1,5 +1,7 @@ +pub mod gemtext; +pub use gemtext::Gemtext; + use super::TAG; -use glib::{Regex, RegexCompileFlags, RegexMatchFlags}; /// Inline [preformatted](https://geminiprotocol.net/docs/gemtext-specification.gmi#in-pre-formatted-mode) entity holder pub struct Inline { @@ -10,24 +12,35 @@ impl Inline { // Constructors /// Parse `Self` from line string - pub fn from(line: &str) -> Option { - // Skip next operations on prefix and postfix mismatch `TAG` - // * replace regex implementation @TODO - if !line.starts_with(TAG) && !line.ends_with(TAG) { - return None; - } - - // Parse line - let regex = Regex::split_simple( - r"^`{3}([^`]+)`{3}$", - line, - RegexCompileFlags::DEFAULT, - RegexMatchFlags::DEFAULT, - ); - - // Extract formatted value - Some(Self { - value: regex.get(1)?.trim().to_string(), + pub fn parse(line: &str) -> Option { + line.as_value().map(|v| Self { + value: v.to_string(), }) } + + // Converters + + /// Convert `Self` to [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) line + pub fn to_source(&self) -> String { + self.value.to_source() + } +} + +#[test] +fn test() { + fn assert(source: &str, value: &str) { + let list = Inline::parse(source).unwrap(); + assert_eq!(list.value, value); + assert_eq!(list.to_source(), format!("```{value}```")); + } + assert("```inline```", "inline"); + assert("```inline ```", "inline"); + assert("``` inline ```", "inline"); + assert("``` inline```", "inline"); + assert("``` inline``` ", "inline"); + assert("``````inline``` ", "```inline"); + assert("``````inline`````` ", "```inline```"); + assert("```inline`````` ", "inline```"); + assert!("```inline".as_value().is_none()); + assert!("```inline``` ne".as_value().is_none()); } diff --git a/src/line/code/inline/gemtext.rs b/src/line/code/inline/gemtext.rs new file mode 100644 index 0000000..ba53570 --- /dev/null +++ b/src/line/code/inline/gemtext.rs @@ -0,0 +1,38 @@ +use super::TAG; + +pub trait Gemtext { + /// Get [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) value for `Self` + fn as_value(&self) -> Option<&str>; + /// Convert `Self` to [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) line + fn to_source(&self) -> String; +} + +impl Gemtext for str { + fn as_value(&self) -> Option<&str> { + if let Some(p) = self.strip_prefix(TAG) { + return p.trim().strip_suffix(TAG).map(|s| s.trim()); + } + None + } + fn to_source(&self) -> String { + format!("{TAG}{}{TAG}", self.trim()) + } +} + +#[test] +fn test() { + fn assert(source: &str, value: &str) { + assert_eq!(source.as_value(), Some(value)); + assert_eq!(value.to_source(), format!("```{value}```")); + } + assert("```inline```", "inline"); + assert("```inline ```", "inline"); + assert("``` inline ```", "inline"); + assert("``` inline```", "inline"); + assert("``` inline``` ", "inline"); + assert("``````inline``` ", "```inline"); + assert("``````inline`````` ", "```inline```"); + assert("```inline`````` ", "inline```"); + assert!("```inline".as_value().is_none()); + assert!("```inline``` ne".as_value().is_none()); +} diff --git a/tests/integration.rs b/tests/integration.rs index 70924f6..c9168fe 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -36,7 +36,7 @@ fn gemtext() { // Parse document by line for line in gemtext.lines() { // Inline code - if let Some(result) = Inline::from(line) { + if let Some(result) = Inline::parse(line) { code_inline.push(result); continue; } From 92e1557c9c0697f4fef42ca9c537fe21aef26ada Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 17 Mar 2025 02:39:26 +0200 Subject: [PATCH 093/105] update readme --- README.md | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 01ef74f..59a297c 100644 --- a/README.md +++ b/README.md @@ -20,20 +20,6 @@ cargo add ggemtext Line parser, useful for [TextTag](https://docs.gtk.org/gtk4/class.TextTag.html) operations in [TextBuffer](https://docs.gtk.org/gtk4/class.TextBuffer.html) context. -**Connect dependencies** - -``` rust -use ggemtext::line::{ - code::{Inline, Multiline}, - header::{Header, Level}, - link::Link, - list::List, - quote::Quote, -} -``` - -**Prepare document** - Iterate Gemtext lines to continue with [Line](#Line) API: ``` rust @@ -47,6 +33,7 @@ for line in gemtext.lines() { ##### Inline ``` rust +use ggemtext::line::code::Inline; match Inline::from("```inline```") { Some(inline) => assert_eq!(inline.value, "inline"), None => assert!(false), @@ -56,6 +43,7 @@ match Inline::from("```inline```") { ##### Multiline ``` rust +use ggemtext::line::code::Multiline; match Multiline::begin_from("```alt") { Some(mut multiline) => { assert!(Multiline::continue_from(&mut multiline, "line 1").is_ok()); @@ -75,6 +63,7 @@ match Multiline::begin_from("```alt") { **Struct** ``` rust +use ggemtext::line::{Header, header::Level}; match Header::parse("# H1") { Some(h1) => { assert_eq!(h1.level as u8, Level::H1 as u8); @@ -96,6 +85,7 @@ assert_eq!("H1".to_source(&Level::H1), "# H1"); #### Link ``` rust +use ggemtext::line::Link; match Link::from( "=> gemini://geminiprotocol.net 1965-01-19 Gemini", None, // absolute path given, base not wanted @@ -127,6 +117,7 @@ match Link::from( **Struct** ``` rust +use ggemtext::line::List; match List::parse("* Item") { Some(list) => assert_eq!(list.value, "Item"), None => assert!(false), @@ -146,6 +137,7 @@ assert_eq!("Item".to_source(), "* Item") **Struct** ``` rust +use ggemtext::line::Quote; match Quote::parse("> Quote") { Some(quote) => assert_eq!(quote.value, "Quote"), None => assert!(false), From 0c6ba0c87cba77885e5161692599c2cc5cdba73b Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 17 Mar 2025 02:42:22 +0200 Subject: [PATCH 094/105] add trait example --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 59a297c..5cba819 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,8 @@ for line in gemtext.lines() { ##### Inline +**Struct** + ``` rust use ggemtext::line::code::Inline; match Inline::from("```inline```") { @@ -40,6 +42,14 @@ match Inline::from("```inline```") { } ``` +**Trait** + +``` rust +use ggemtext::line::code::inline::Gemtext; +assert_eq!("```inline```".as_value(), Some("inline")) +assert_eq!("inline".to_source(), "```inline```") +``` + ##### Multiline ``` rust From 0c90bbafbac886468c3aa60fcda2346c3f7154d3 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 17 Mar 2025 02:46:13 +0200 Subject: [PATCH 095/105] update headers --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5cba819..cc19618 100644 --- a/README.md +++ b/README.md @@ -28,9 +28,7 @@ for line in gemtext.lines() { } ``` -#### Code - -##### Inline +#### Inline code **Struct** @@ -50,7 +48,7 @@ assert_eq!("```inline```".as_value(), Some("inline")) assert_eq!("inline".to_source(), "```inline```") ``` -##### Multiline +#### Multiline code ``` rust use ggemtext::line::code::Multiline; From 22a05a975c95bd8ce1545fc5bccc10dae4120ce3 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 17 Mar 2025 21:39:07 +0200 Subject: [PATCH 096/105] remove regex dependency, rename constructor, add tests --- README.md | 42 +++++----- src/line/link.rs | 189 +++++++++++++++++++++++-------------------- tests/integration.rs | 93 ++++++++++++--------- 3 files changed, 177 insertions(+), 147 deletions(-) diff --git a/README.md b/README.md index cc19618..8bbfde8 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ for line in gemtext.lines() { ``` rust use ggemtext::line::code::Inline; -match Inline::from("```inline```") { +match Inline::parse("```inline```") { Some(inline) => assert_eq!(inline.value, "inline"), None => assert!(false), } @@ -93,31 +93,25 @@ assert_eq!("H1".to_source(&Level::H1), "# H1"); #### Link ``` rust -use ggemtext::line::Link; -match Link::from( - "=> gemini://geminiprotocol.net 1965-01-19 Gemini", - None, // absolute path given, base not wanted - Some(&glib::TimeZone::local()), -) { - Some(link) => { - // Alt - assert_eq!(link.alt, Some("Gemini".into())); +use crate::line::Link; - // Date - match link.timestamp { - Some(timestamp) => { - assert_eq!(timestamp.year(), 1965); - assert_eq!(timestamp.month(), 1); - assert_eq!(timestamp.day_of_month(), 19); - } - None => assert!(false), - } +const SOURCE: &str = "=> gemini://geminiprotocol.net 1965-01-19 Gemini"; - // URI - assert_eq!(link.uri.to_string(), "gemini://geminiprotocol.net"); - } - None => assert!(false), -} +let link = Link::parse(SOURCE).unwrap(); + +assert_eq!(link.alt, Some("1965-01-19 Gemini".to_string())); +assert_eq!(link.url, "gemini://geminiprotocol.net"); + +let uri = link.uri(None).unwrap(); +assert_eq!(uri.scheme(), "gemini"); +assert_eq!(uri.host().unwrap(), "geminiprotocol.net"); + +let time = link.time(Some(&glib::TimeZone::local())).unwrap(); +assert_eq!(time.year(), 1965); +assert_eq!(time.month(), 1); +assert_eq!(time.day_of_month(), 19); + +assert_eq!(link.to_source(), SOURCE); ``` #### List diff --git a/src/line/link.rs b/src/line/link.rs index 4b7d4bc..d95cd47 100644 --- a/src/line/link.rs +++ b/src/line/link.rs @@ -1,104 +1,119 @@ -use glib::{DateTime, Regex, RegexCompileFlags, RegexMatchFlags, TimeZone, Uri, UriFlags}; +use glib::{DateTime, TimeZone, Uri, UriFlags}; +const S: char = ' '; pub const TAG: &str = "=>"; /// [Link](https://geminiprotocol.net/docs/gemtext-specification.gmi#link-lines) entity holder pub struct Link { - pub alt: Option, // [optional] alternative link description - pub timestamp: Option, // [optional] valid link DateTime object - pub uri: Uri, // [required] valid link URI object + /// For performance reasons, hold Gemtext date and alternative together as the optional String + /// * to extract valid [DateTime](https://docs.gtk.org/glib/struct.DateTime.html) use `time` implementation method + pub alt: Option, + /// For performance reasons, hold URL as the raw String + /// * to extract valid [Uri](https://docs.gtk.org/glib/struct.Uri.html) use `uri` implementation method + pub url: String, } impl Link { // Constructors /// Parse `Self` from line string - pub fn from(line: &str, base: Option<&Uri>, timezone: Option<&TimeZone>) -> Option { - // Skip next operations on prefix mismatch - // * replace regex implementation @TODO - if !line.starts_with(TAG) { + pub fn parse(line: &str) -> Option { + let l = line.strip_prefix(TAG)?.trim(); + let u = l.find(S).map_or(l, |i| &l[..i]); + if u.is_empty() { return None; } - - // Define initial values - let mut alt = None; - let mut timestamp = 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 mut unresolved_address = regex.get(1)?.to_string(); - - // Relative scheme patch - // https://datatracker.ietf.org/doc/html/rfc3986#section-4.2 - if let Some(p) = unresolved_address.strip_prefix("//") { - let b = base?; - let postfix = p.trim_start_matches(":"); - unresolved_address = format!( - "{}://{}", - b.scheme(), - if postfix.is_empty() { - format!("{}/", b.host()?) - } else { - postfix.into() - } - ) - } - // 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) => resolved_uri, - Err(_) => return None, - } - } - Err(_) => return None, - } - } - // Base resolve not requested - None => { - // 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) { - 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) { - if !value.is_empty() { - alt = Some(value.to_string()) - } - }; - Some(Self { - alt, - timestamp, - uri, + alt: l + .get(u.len()..) + .map(|a| a.trim()) + .filter(|a| !a.is_empty()) + .map(|a| a.to_string()), + url: u.to_string(), }) } + + // Converters + + /// Convert `Self` to [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) line + pub fn to_source(&self) -> String { + let mut s = String::with_capacity( + TAG.len() + self.url.len() + self.alt.as_ref().map_or(0, |a| a.len()) + 2, + ); + s.push_str(TAG); + s.push(S); + s.push_str(&self.url); + if let Some(ref alt) = self.alt { + s.push(S); + s.push_str(alt); + } + s + } + + // Getters + + /// Get valid [DateTime](https://docs.gtk.org/glib/struct.DateTime.html) for `Self` + pub fn time(&self, timezone: Option<&TimeZone>) -> Option { + let a = self.alt.as_ref()?; + let t = &a[..a.find(S).unwrap_or(a.len())]; + DateTime::from_iso8601(&format!("{t}T00:00:00"), timezone).ok() + } + + /// Get valid [Uri](https://docs.gtk.org/glib/struct.Uri.html) for `Self` + pub fn uri(&self, base: Option<&Uri>) -> Option { + // Relative scheme patch + // https://datatracker.ietf.org/doc/html/rfc3986#section-4.2 + let unresolved_address = match self.url.strip_prefix("//") { + Some(p) => { + let b = base?; + let s = p.trim_start_matches(":"); + &format!( + "{}://{}", + b.scheme(), + if s.is_empty() { + format!("{}/", b.host()?) + } else { + s.into() + } + ) + } + None => &self.url, + }; + // Convert address to the valid URI, + // resolve to absolute URL format if the target is relative + match base { + Some(base_uri) => match Uri::resolve_relative( + Some(&base_uri.to_str()), + unresolved_address, + UriFlags::NONE, + ) { + Ok(resolved_str) => Uri::parse(&resolved_str, UriFlags::NONE).ok(), + Err(_) => None, + }, + None => Uri::parse(unresolved_address, UriFlags::NONE).ok(), + } + } +} + +#[test] +fn test() { + use crate::line::Link; + + const SOURCE: &str = "=> gemini://geminiprotocol.net 1965-01-19 Gemini"; + + let link = Link::parse(SOURCE).unwrap(); + + assert_eq!(link.alt, Some("1965-01-19 Gemini".to_string())); + assert_eq!(link.url, "gemini://geminiprotocol.net"); + + let uri = link.uri(None).unwrap(); + assert_eq!(uri.scheme(), "gemini"); + assert_eq!(uri.host().unwrap(), "geminiprotocol.net"); + + let time = link.time(Some(&glib::TimeZone::local())).unwrap(); + assert_eq!(time.year(), 1965); + assert_eq!(time.month(), 1); + assert_eq!(time.day_of_month(), 19); + + assert_eq!(link.to_source(), SOURCE); } diff --git a/tests/integration.rs b/tests/integration.rs index c9168fe..faf24ff 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -25,13 +25,10 @@ fn gemtext() { let mut code_multiline_buffer: Option = None; // Define base URI as integration.gmi contain one relative link - let base = match Uri::parse("gemini://geminiprotocol.net", UriFlags::NONE) { - Ok(uri) => Some(uri), - Err(_) => None, - }; + let base = Uri::parse("gemini://geminiprotocol.net", UriFlags::NONE).unwrap(); // Define timezone as integration.gmi contain one links with date - let timezone = Some(TimeZone::local()); + let timezone = TimeZone::local(); // Parse document by line for line in gemtext.lines() { @@ -66,7 +63,7 @@ fn gemtext() { } // Link - if let Some(result) = Link::from(line, base.as_ref(), timezone.as_ref()) { + if let Some(result) = Link::parse(line) { links.push(result); continue; } @@ -150,52 +147,64 @@ fn gemtext() { let item = link.next().unwrap(); assert_eq!(item.alt, None); - assert_eq!(item.timestamp, None); - assert_eq!(item.uri.to_str(), "gemini://geminiprotocol.net"); + assert_eq!(item.time(Some(&timezone)), None); + assert_eq!( + item.uri(Some(&base)).unwrap().to_str(), + "gemini://geminiprotocol.net" + ); } // #1 { let item = link.next().unwrap(); - assert_eq!(item.alt, None); + assert_eq!( + item.uri(Some(&base)).unwrap().to_string(), + "gemini://geminiprotocol.net" + ); - let timestamp = item.timestamp.clone().unwrap(); - assert_eq!(timestamp.year(), 1965); - assert_eq!(timestamp.month(), 1); - assert_eq!(timestamp.day_of_month(), 19); + let time = item.time(Some(&timezone)).unwrap(); + assert_eq!(time.year(), 1965); + assert_eq!(time.month(), 1); + assert_eq!(time.day_of_month(), 19); - assert_eq!(item.uri.to_str(), "gemini://geminiprotocol.net"); + assert_eq!(item.alt, Some("1965-01-19".to_string())); } // #2 { let item = link.next().unwrap(); assert_eq!(item.alt.clone().unwrap(), "Gemini"); - assert_eq!(item.timestamp, None); - assert_eq!(item.uri.to_str(), "gemini://geminiprotocol.net"); + assert_eq!(item.time(Some(&timezone)), None); + assert_eq!( + item.uri(Some(&base)).unwrap().to_string(), + "gemini://geminiprotocol.net" + ); } // #3 { let item = link.next().unwrap(); - assert_eq!(item.alt.clone().unwrap(), "Gemini"); + assert_eq!(item.alt, Some("1965-01-19 Gemini".to_string())); - let timestamp = item.timestamp.clone().unwrap(); - assert_eq!(timestamp.year(), 1965); - assert_eq!(timestamp.month(), 1); - assert_eq!(timestamp.day_of_month(), 19); + let time = item.time(Some(&timezone)).unwrap(); + assert_eq!(time.year(), 1965); + assert_eq!(time.month(), 1); + assert_eq!(time.day_of_month(), 19); - assert_eq!(item.uri.to_str(), "gemini://geminiprotocol.net"); + assert_eq!( + item.uri(Some(&base)).unwrap().to_string(), + "gemini://geminiprotocol.net" + ); } // #4 { let item = link.next().unwrap(); - assert_eq!(item.alt.clone().unwrap(), "Gemini"); + assert_eq!(item.alt, Some("1965-01-19 Gemini".to_string())); - let timestamp = item.timestamp.clone().unwrap(); - assert_eq!(timestamp.year(), 1965); - assert_eq!(timestamp.month(), 1); - assert_eq!(timestamp.day_of_month(), 19); + let time = item.time(Some(&timezone)).unwrap(); + assert_eq!(time.year(), 1965); + assert_eq!(time.month(), 1); + assert_eq!(time.day_of_month(), 19); assert_eq!( - item.uri.to_str(), + item.uri(Some(&base)).unwrap().to_string(), "gemini://geminiprotocol.net/docs/gemtext.gmi" ); } // #5 @@ -203,29 +212,41 @@ fn gemtext() { let item = link.next().unwrap(); assert_eq!(item.alt, None); - assert_eq!(item.timestamp, None); - assert_eq!(item.uri.to_str(), "gemini://geminiprotocol.net"); + assert_eq!(item.time(Some(&timezone)), None); + assert_eq!( + item.uri(Some(&base)).unwrap().to_string(), + "gemini://geminiprotocol.net" + ); } // #6 { let item = link.next().unwrap(); assert_eq!(item.alt, None); - assert_eq!(item.timestamp, None); - assert_eq!(item.uri.to_str(), "gemini://geminiprotocol.net"); + assert_eq!(item.time(Some(&timezone)), None); + assert_eq!( + item.uri(Some(&base)).unwrap().to_string(), + "gemini://geminiprotocol.net" + ); } // #7 { let item = link.next().unwrap(); assert_eq!(item.alt, None); - assert_eq!(item.timestamp, None); - assert_eq!(item.uri.to_str(), "gemini://geminiprotocol.net/path"); + assert_eq!(item.time(Some(&timezone)), None); + assert_eq!( + item.uri(Some(&base)).unwrap().to_string(), + "gemini://geminiprotocol.net/path" + ); } // #8 { let item = link.next().unwrap(); assert_eq!(item.alt, None); - assert_eq!(item.timestamp, None); - assert_eq!(item.uri.to_str(), "gemini://geminiprotocol.net/"); + assert_eq!(item.time(Some(&timezone)), None); + assert_eq!( + item.uri(Some(&base)).unwrap().to_string(), + "gemini://geminiprotocol.net/" + ); } // #9 // Validate lists From 841ee2036e11db0d7efbc994ce5f267a97d508aa Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 17 Mar 2025 21:42:09 +0200 Subject: [PATCH 097/105] fix namespace example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8bbfde8..a3809da 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ assert_eq!("H1".to_source(&Level::H1), "# H1"); #### Link ``` rust -use crate::line::Link; +use ggemtext::line::Link; const SOURCE: &str = "=> gemini://geminiprotocol.net 1965-01-19 Gemini"; From 25a45337ff1acd11773db1f5b292fcc97e0e139d Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 17 Mar 2025 22:23:07 +0200 Subject: [PATCH 098/105] trim members --- src/line/link.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/line/link.rs b/src/line/link.rs index d95cd47..0d2aff9 100644 --- a/src/line/link.rs +++ b/src/line/link.rs @@ -42,10 +42,10 @@ impl Link { ); s.push_str(TAG); s.push(S); - s.push_str(&self.url); + s.push_str(self.url.trim()); if let Some(ref alt) = self.alt { s.push(S); - s.push_str(alt); + s.push_str(alt.trim()); } s } From 10ff400d8fd214f7b73ac18506fdf96c6c335b41 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 18 Mar 2025 01:05:23 +0200 Subject: [PATCH 099/105] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 72faff5..2a9b4c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemtext" -version = "0.5.0" +version = "0.5.1" edition = "2024" license = "MIT" readme = "README.md" From 678f906f48b5060d07123b1939ab39ca0fbbaf31 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 21 Mar 2025 17:02:50 +0200 Subject: [PATCH 100/105] remove unspecified inline code tag support --- src/line.rs | 1 + src/line/code.rs | 93 ++++++++++++++++++++++++-- src/line/code/{multiline => }/error.rs | 0 src/line/code/inline.rs | 46 ------------- src/line/code/inline/gemtext.rs | 38 ----------- src/line/code/multiline.rs | 59 ---------------- tests/integration.gmi | 2 - tests/integration.rs | 46 +++++-------- 8 files changed, 104 insertions(+), 181 deletions(-) rename src/line/code/{multiline => }/error.rs (100%) delete mode 100644 src/line/code/inline.rs delete mode 100644 src/line/code/inline/gemtext.rs delete mode 100644 src/line/code/multiline.rs diff --git a/src/line.rs b/src/line.rs index 3b1a1fb..2cf472a 100644 --- a/src/line.rs +++ b/src/line.rs @@ -4,6 +4,7 @@ pub mod link; pub mod list; pub mod quote; +pub use code::Code; pub use header::Header; pub use link::Link; pub use list::List; diff --git a/src/line/code.rs b/src/line/code.rs index 60345bb..aa410b5 100644 --- a/src/line/code.rs +++ b/src/line/code.rs @@ -1,7 +1,90 @@ -pub mod inline; -pub mod multiline; - -pub use inline::Inline; -pub use multiline::Multiline; +pub mod error; +pub use error::Error; pub const TAG: &str = "```"; +pub const NEW_LINE: char = '\n'; + +/// Multi-line [preformatted](https://geminiprotocol.net/docs/gemtext-specification.gmi#in-pre-formatted-mode) entity holder +pub struct Code { + pub alt: Option, + pub value: String, + pub is_completed: bool, +} + +impl Code { + // Constructors + + /// Search in line string for tag open, + /// return Self constructed on success or None + pub fn begin_from(line: &str) -> Option { + if line.starts_with(TAG) { + let alt = line.trim_start_matches(TAG).trim(); + + return Some(Self { + alt: match alt.is_empty() { + true => None, + false => Some(alt.to_string()), + }, + value: String::new(), + is_completed: false, + }); + } + None + } + + /// Continue preformatted buffer from line string, + /// set `completed` as True on close tag found + pub fn continue_from(&mut self, line: &str) -> Result<(), Error> { + // Make sure buffer not completed yet + if self.is_completed { + return Err(Error::Completed); + } + + // Append to value, trim close tag on exists + self.value.push_str(line.trim_end_matches(TAG)); + + // Line contain close tag + if line.ends_with(TAG) { + self.is_completed = true; + } else { + self.value.push(NEW_LINE); + } + + Ok(()) + } + + // Converters + + /// Convert `Self` to [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) format + pub fn to_source(&self) -> String { + format!( + "{TAG}{}{NEW_LINE}{}{TAG}", + match &self.alt { + Some(alt) => format!(" {}", alt.trim()), + None => String::new(), + }, + self.value + ) + } +} + +#[test] +fn test() { + match Code::begin_from("```alt") { + Some(mut code) => { + assert!(code.continue_from("line 1").is_ok()); + assert!(code.continue_from("line 2").is_ok()); + assert!(code.continue_from("```").is_ok()); // complete + + assert!(code.is_completed); + assert_eq!(code.alt, Some("alt".into())); + assert_eq!(code.value.len(), 12 + 2); // +NL + + assert_eq!( + code.to_source(), + format!("{TAG} alt{NEW_LINE}line 1{NEW_LINE}line 2{NEW_LINE}{TAG}") + ) + } + None => assert!(false), + } +} diff --git a/src/line/code/multiline/error.rs b/src/line/code/error.rs similarity index 100% rename from src/line/code/multiline/error.rs rename to src/line/code/error.rs diff --git a/src/line/code/inline.rs b/src/line/code/inline.rs deleted file mode 100644 index a3cbc01..0000000 --- a/src/line/code/inline.rs +++ /dev/null @@ -1,46 +0,0 @@ -pub mod gemtext; -pub use gemtext::Gemtext; - -use super::TAG; - -/// Inline [preformatted](https://geminiprotocol.net/docs/gemtext-specification.gmi#in-pre-formatted-mode) entity holder -pub struct Inline { - pub value: String, -} - -impl Inline { - // Constructors - - /// Parse `Self` from line string - pub fn parse(line: &str) -> Option { - line.as_value().map(|v| Self { - value: v.to_string(), - }) - } - - // Converters - - /// Convert `Self` to [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) line - pub fn to_source(&self) -> String { - self.value.to_source() - } -} - -#[test] -fn test() { - fn assert(source: &str, value: &str) { - let list = Inline::parse(source).unwrap(); - assert_eq!(list.value, value); - assert_eq!(list.to_source(), format!("```{value}```")); - } - assert("```inline```", "inline"); - assert("```inline ```", "inline"); - assert("``` inline ```", "inline"); - assert("``` inline```", "inline"); - assert("``` inline``` ", "inline"); - assert("``````inline``` ", "```inline"); - assert("``````inline`````` ", "```inline```"); - assert("```inline`````` ", "inline```"); - assert!("```inline".as_value().is_none()); - assert!("```inline``` ne".as_value().is_none()); -} diff --git a/src/line/code/inline/gemtext.rs b/src/line/code/inline/gemtext.rs deleted file mode 100644 index ba53570..0000000 --- a/src/line/code/inline/gemtext.rs +++ /dev/null @@ -1,38 +0,0 @@ -use super::TAG; - -pub trait Gemtext { - /// Get [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) value for `Self` - fn as_value(&self) -> Option<&str>; - /// Convert `Self` to [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) line - fn to_source(&self) -> String; -} - -impl Gemtext for str { - fn as_value(&self) -> Option<&str> { - if let Some(p) = self.strip_prefix(TAG) { - return p.trim().strip_suffix(TAG).map(|s| s.trim()); - } - None - } - fn to_source(&self) -> String { - format!("{TAG}{}{TAG}", self.trim()) - } -} - -#[test] -fn test() { - fn assert(source: &str, value: &str) { - assert_eq!(source.as_value(), Some(value)); - assert_eq!(value.to_source(), format!("```{value}```")); - } - assert("```inline```", "inline"); - assert("```inline ```", "inline"); - assert("``` inline ```", "inline"); - assert("``` inline```", "inline"); - assert("``` inline``` ", "inline"); - assert("``````inline``` ", "```inline"); - assert("``````inline`````` ", "```inline```"); - assert("```inline`````` ", "inline```"); - assert!("```inline".as_value().is_none()); - assert!("```inline``` ne".as_value().is_none()); -} diff --git a/src/line/code/multiline.rs b/src/line/code/multiline.rs deleted file mode 100644 index 2e1ef32..0000000 --- a/src/line/code/multiline.rs +++ /dev/null @@ -1,59 +0,0 @@ -use super::TAG; - -pub mod error; -pub use error::Error; - -// Shared defaults - -pub const NEW_LINE: char = '\n'; - -/// Multi-line [preformatted](https://geminiprotocol.net/docs/gemtext-specification.gmi#in-pre-formatted-mode) entity holder -pub struct Multiline { - pub alt: Option, - pub value: String, - pub completed: bool, -} - -impl Multiline { - // Constructors - - /// Search in line string for tag open, - /// return Self constructed on success or None - pub fn begin_from(line: &str) -> Option { - if line.starts_with(TAG) { - let alt = line.trim_start_matches(TAG).trim(); - - return Some(Self { - alt: match alt.is_empty() { - true => None, - false => Some(alt.to_string()), - }, - value: String::new(), - completed: false, - }); - } - - None - } - - /// Continue preformatted buffer from line string, - /// set `completed` as True on close tag found - pub fn continue_from(&mut self, line: &str) -> Result<(), Error> { - // Make sure buffer not completed yet - if self.completed { - return Err(Error::Completed); - } - - // Append to value, trim close tag on exists - self.value.push_str(line.trim_end_matches(TAG)); - - // Line contain close tag - if line.ends_with(TAG) { - self.completed = true; - } else { - self.value.push(NEW_LINE); - } - - Ok(()) - } -} diff --git a/tests/integration.gmi b/tests/integration.gmi index a33000f..400695a 100644 --- a/tests/integration.gmi +++ b/tests/integration.gmi @@ -15,8 +15,6 @@ * Listing item 1 * Listing item 2 -```inline code``` - ``` alt text multi preformatted line diff --git a/tests/integration.rs b/tests/integration.rs index faf24ff..d96ccff 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1,9 +1,6 @@ use ggemtext::line::{ - code::{Inline, Multiline}, + Code, Link, List, Quote, header::{Header, Level}, - link::Link, - list::List, - quote::Quote, }; use glib::{TimeZone, Uri, UriFlags}; @@ -14,15 +11,14 @@ fn gemtext() { match fs::read_to_string("tests/integration.gmi") { Ok(gemtext) => { // Init tags collection - let mut code_inline: Vec = Vec::new(); - let mut code_multiline: Vec = Vec::new(); + let mut code: Vec = Vec::new(); let mut headers: Vec
= Vec::new(); let mut links: Vec = Vec::new(); let mut list: Vec = Vec::new(); let mut quote: Vec = Vec::new(); // Define preformatted buffer - let mut code_multiline_buffer: Option = None; + let mut code_buffer: Option = None; // Define base URI as integration.gmi contain one relative link let base = Uri::parse("gemini://geminiprotocol.net", UriFlags::NONE).unwrap(); @@ -32,25 +28,18 @@ fn gemtext() { // Parse document by line for line in gemtext.lines() { - // Inline code - if let Some(result) = Inline::parse(line) { - code_inline.push(result); - continue; - } - - // Multiline code - match code_multiline_buffer { + match code_buffer { None => { - if let Some(code) = Multiline::begin_from(line) { - code_multiline_buffer = Some(code); + if let Some(code) = Code::begin_from(line) { + code_buffer = Some(code); continue; } } - Some(ref mut result) => { - assert!(Multiline::continue_from(result, line).is_ok()); - if result.completed { - code_multiline.push(code_multiline_buffer.take().unwrap()); - code_multiline_buffer = None; + Some(ref mut c) => { + assert!(c.continue_from(line).is_ok()); + if c.is_completed { + code.push(code_buffer.take().unwrap()); + code_buffer = None; } continue; } @@ -81,15 +70,10 @@ fn gemtext() { } } - // Validate inline code - assert_eq!(code_inline.len(), 1); - assert_eq!(code_inline.first().unwrap().value, "inline code"); - - // Validate multiline code - assert_eq!(code_multiline.len(), 2); - + // Validate code + assert_eq!(code.len(), 2); { - let item = code_multiline.first().unwrap(); + let item = code.first().unwrap(); assert_eq!(item.alt.clone().unwrap(), "alt text"); assert_eq!(item.value.lines().count(), 2); @@ -100,7 +84,7 @@ fn gemtext() { } // #1 { - let item = code_multiline.get(1).unwrap(); + let item = code.get(1).unwrap(); assert_eq!(item.alt.clone(), None); assert_eq!(item.value.lines().count(), 2); From ff70e5410acd0a96a83dad40d33abf33b9b52a9f Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 21 Mar 2025 17:02:58 +0200 Subject: [PATCH 101/105] update examples --- README.md | 40 ++++++++++------------------------------ 1 file changed, 10 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index a3809da..b6703bc 100644 --- a/README.md +++ b/README.md @@ -28,39 +28,19 @@ for line in gemtext.lines() { } ``` -#### Inline code - -**Struct** +#### Code ``` rust -use ggemtext::line::code::Inline; -match Inline::parse("```inline```") { - Some(inline) => assert_eq!(inline.value, "inline"), - None => assert!(false), -} -``` +use ggemtext::line::Code; +match Code::begin_from("```alt") { + Some(mut code) => { + assert!(code.continue_from("line 1").is_ok()); + assert!(code.continue_from("line 2").is_ok()); + assert!(code.continue_from("```").is_ok()); // complete -**Trait** - -``` rust -use ggemtext::line::code::inline::Gemtext; -assert_eq!("```inline```".as_value(), Some("inline")) -assert_eq!("inline".to_source(), "```inline```") -``` - -#### Multiline code - -``` rust -use ggemtext::line::code::Multiline; -match Multiline::begin_from("```alt") { - Some(mut multiline) => { - assert!(Multiline::continue_from(&mut multiline, "line 1").is_ok()); - assert!(Multiline::continue_from(&mut multiline, "line 2").is_ok()); - assert!(Multiline::continue_from(&mut multiline, "```").is_ok()); // complete - - assert!(multiline.completed); - assert_eq!(multiline.alt, Some("alt".into())); - assert_eq!(multiline.buffer.len(), 3); + assert!(code.is_completed); + assert_eq!(code.alt, Some("alt".into())); + assert_eq!(code.value.len(), 12 + 2); // +NL } None => assert!(false), } From 459626acb499742786ab13b91f68353d440b3ab1 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 21 Mar 2025 17:03:10 +0200 Subject: [PATCH 102/105] update minor version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 2a9b4c8..1de2359 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemtext" -version = "0.5.1" +version = "0.6.0" edition = "2024" license = "MIT" readme = "README.md" From 2d98b66d82a8cad652d3c61455d1b06200f59aa0 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 21 Mar 2025 17:47:43 +0200 Subject: [PATCH 103/105] fix clippy --- README.md | 8 ++++---- src/line/code.rs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b6703bc..59d9f1d 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ match Code::begin_from("```alt") { assert_eq!(code.alt, Some("alt".into())); assert_eq!(code.value.len(), 12 + 2); // +NL } - None => assert!(false), + None => unreachable!(), } ``` @@ -57,7 +57,7 @@ match Header::parse("# H1") { assert_eq!(h1.level as u8, Level::H1 as u8); assert_eq!(h1.value, "H1"); } - None => assert!(false), + None => unreachable!(), } // H1, H2, H3 ``` @@ -102,7 +102,7 @@ assert_eq!(link.to_source(), SOURCE); use ggemtext::line::List; match List::parse("* Item") { Some(list) => assert_eq!(list.value, "Item"), - None => assert!(false), + None => unreachable!(), } ``` @@ -122,7 +122,7 @@ assert_eq!("Item".to_source(), "* Item") use ggemtext::line::Quote; match Quote::parse("> Quote") { Some(quote) => assert_eq!(quote.value, "Quote"), - None => assert!(false), + None => unreachable!(), } ``` diff --git a/src/line/code.rs b/src/line/code.rs index aa410b5..165a308 100644 --- a/src/line/code.rs +++ b/src/line/code.rs @@ -85,6 +85,6 @@ fn test() { format!("{TAG} alt{NEW_LINE}line 1{NEW_LINE}line 2{NEW_LINE}{TAG}") ) } - None => assert!(false), + None => unreachable!(), } } From f83d049c60b24007e544e0b61bc273a97fdddd42 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 28 Mar 2025 00:30:15 +0200 Subject: [PATCH 104/105] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 1de2359..cb25ba6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemtext" -version = "0.6.0" +version = "0.6.1" edition = "2024" license = "MIT" readme = "README.md" From 2a51b6738709ed47987ffbe82eb258576a2e50fe Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 23 Jul 2025 05:08:52 +0300 Subject: [PATCH 105/105] update `glib` version to `0.21.0`, update crate version to `0.7.0` --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cb25ba6..4d36cc5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemtext" -version = "0.6.1" +version = "0.7.0" edition = "2024" license = "MIT" readme = "README.md" @@ -17,5 +17,5 @@ repository = "https://github.com/YGGverse/ggemtext" [dependencies.glib] package = "glib" -version = "0.20.9" +version = "0.21.0" features = ["v2_66"]