diff --git a/Cargo.toml b/Cargo.toml index 4d36cc5..72faff5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemtext" -version = "0.7.0" +version = "0.5.0" edition = "2024" license = "MIT" readme = "README.md" @@ -17,5 +17,5 @@ repository = "https://github.com/YGGverse/ggemtext" [dependencies.glib] package = "glib" -version = "0.21.0" +version = "0.20.9" features = ["v2_66"] diff --git a/README.md b/README.md index 59d9f1d..a3809da 100644 --- a/README.md +++ b/README.md @@ -28,21 +28,41 @@ for line in gemtext.lines() { } ``` -#### Code +#### Inline code + +**Struct** ``` rust -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 +use ggemtext::line::code::Inline; +match Inline::parse("```inline```") { + Some(inline) => assert_eq!(inline.value, "inline"), + None => assert!(false), +} +``` - assert!(code.is_completed); - assert_eq!(code.alt, Some("alt".into())); - assert_eq!(code.value.len(), 12 + 2); // +NL +**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); } - None => unreachable!(), + None => assert!(false), } ``` @@ -57,7 +77,7 @@ match Header::parse("# H1") { assert_eq!(h1.level as u8, Level::H1 as u8); assert_eq!(h1.value, "H1"); } - None => unreachable!(), + None => assert!(false), } // H1, H2, H3 ``` @@ -102,7 +122,7 @@ assert_eq!(link.to_source(), SOURCE); use ggemtext::line::List; match List::parse("* Item") { Some(list) => assert_eq!(list.value, "Item"), - None => unreachable!(), + None => assert!(false), } ``` @@ -122,7 +142,7 @@ assert_eq!("Item".to_source(), "* Item") use ggemtext::line::Quote; match Quote::parse("> Quote") { Some(quote) => assert_eq!(quote.value, "Quote"), - None => unreachable!(), + None => assert!(false), } ``` diff --git a/src/line.rs b/src/line.rs index 2cf472a..3b1a1fb 100644 --- a/src/line.rs +++ b/src/line.rs @@ -4,7 +4,6 @@ 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 165a308..60345bb 100644 --- a/src/line/code.rs +++ b/src/line/code.rs @@ -1,90 +1,7 @@ -pub mod error; -pub use error::Error; +pub mod inline; +pub mod multiline; + +pub use inline::Inline; +pub use multiline::Multiline; 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 => unreachable!(), - } -} diff --git a/src/line/code/inline.rs b/src/line/code/inline.rs new file mode 100644 index 0000000..a3cbc01 --- /dev/null +++ b/src/line/code/inline.rs @@ -0,0 +1,46 @@ +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 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/src/line/code/multiline.rs b/src/line/code/multiline.rs new file mode 100644 index 0000000..2e1ef32 --- /dev/null +++ b/src/line/code/multiline.rs @@ -0,0 +1,59 @@ +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/src/line/code/error.rs b/src/line/code/multiline/error.rs similarity index 100% rename from src/line/code/error.rs rename to src/line/code/multiline/error.rs diff --git a/tests/integration.gmi b/tests/integration.gmi index 400695a..a33000f 100644 --- a/tests/integration.gmi +++ b/tests/integration.gmi @@ -15,6 +15,8 @@ * Listing item 1 * Listing item 2 +```inline code``` + ``` alt text multi preformatted line diff --git a/tests/integration.rs b/tests/integration.rs index d96ccff..faf24ff 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1,6 +1,9 @@ use ggemtext::line::{ - Code, Link, List, Quote, + code::{Inline, Multiline}, header::{Header, Level}, + link::Link, + list::List, + quote::Quote, }; use glib::{TimeZone, Uri, UriFlags}; @@ -11,14 +14,15 @@ fn gemtext() { match fs::read_to_string("tests/integration.gmi") { Ok(gemtext) => { // Init tags collection - let mut code: Vec = Vec::new(); + let mut code_inline: Vec = Vec::new(); + let mut code_multiline: 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_buffer: Option = None; + let mut code_multiline_buffer: Option = None; // Define base URI as integration.gmi contain one relative link let base = Uri::parse("gemini://geminiprotocol.net", UriFlags::NONE).unwrap(); @@ -28,18 +32,25 @@ fn gemtext() { // Parse document by line for line in gemtext.lines() { - match code_buffer { + // Inline code + if let Some(result) = Inline::parse(line) { + code_inline.push(result); + continue; + } + + // Multiline code + match code_multiline_buffer { None => { - if let Some(code) = Code::begin_from(line) { - code_buffer = Some(code); + if let Some(code) = Multiline::begin_from(line) { + code_multiline_buffer = Some(code); continue; } } - Some(ref mut c) => { - assert!(c.continue_from(line).is_ok()); - if c.is_completed { - code.push(code_buffer.take().unwrap()); - code_buffer = None; + 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; } continue; } @@ -70,10 +81,15 @@ fn gemtext() { } } - // Validate code - assert_eq!(code.len(), 2); + // 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); + { - let item = code.first().unwrap(); + let item = code_multiline.first().unwrap(); assert_eq!(item.alt.clone().unwrap(), "alt text"); assert_eq!(item.value.lines().count(), 2); @@ -84,7 +100,7 @@ fn gemtext() { } // #1 { - let item = code.get(1).unwrap(); + let item = code_multiline.get(1).unwrap(); assert_eq!(item.alt.clone(), None); assert_eq!(item.value.lines().count(), 2);