From b37a5f9061f8eec1c238e69ac8c647af306735f0 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 16 Feb 2025 22:36:41 +0200 Subject: [PATCH 01/46] 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 02/46] 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 03/46] 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 04/46] 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 05/46] 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 06/46] 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 07/46] 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 08/46] 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 09/46] 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 10/46] 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 11/46] 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 12/46] 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 13/46] 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 14/46] 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 15/46] 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 16/46] 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 17/46] 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 18/46] 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 19/46] 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 20/46] 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 21/46] 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 22/46] 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 23/46] 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 24/46] 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 25/46] 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 26/46] 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 27/46] 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 28/46] 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 29/46] 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 30/46] 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 31/46] 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 32/46] 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 33/46] 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 34/46] 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 35/46] 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 36/46] 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 37/46] 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 38/46] 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 39/46] 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 40/46] 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 41/46] 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 42/46] 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 43/46] 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 44/46] 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 45/46] 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 46/46] 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"]