Compare commits

...

7 commits
0.5.0 ... main

Author SHA1 Message Date
yggverse
2a51b67387 update glib version to 0.21.0, update crate version to 0.7.0 2025-07-23 05:08:52 +03:00
yggverse
f83d049c60 update version 2025-03-28 00:30:15 +02:00
yggverse
2d98b66d82 fix clippy 2025-03-21 17:47:43 +02:00
yggverse
459626acb4 update minor version 2025-03-21 17:03:10 +02:00
yggverse
ff70e5410a update examples 2025-03-21 17:02:58 +02:00
yggverse
678f906f48 remove unspecified inline code tag support 2025-03-21 17:02:50 +02:00
yggverse
10ff400d8f update version 2025-03-18 01:05:23 +02:00
10 changed files with 120 additions and 217 deletions

View file

@ -1,6 +1,6 @@
[package]
name = "ggemtext"
version = "0.5.0"
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"]

View file

@ -28,41 +28,21 @@ 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),
None => unreachable!(),
}
```
@ -77,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
```
@ -122,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!(),
}
```
@ -142,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!(),
}
```

View file

@ -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;

View file

@ -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<String>,
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<Self> {
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!(),
}
}

View file

@ -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<Self> {
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());
}

View file

@ -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());
}

View file

@ -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<String>,
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<Self> {
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(())
}
}

View file

@ -15,8 +15,6 @@
* Listing item 1
* Listing item 2
```inline code```
``` alt text
multi
preformatted line

View file

@ -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<Inline> = Vec::new();
let mut code_multiline: Vec<Multiline> = Vec::new();
let mut code: Vec<Code> = Vec::new();
let mut headers: Vec<Header> = Vec::new();
let mut links: Vec<Link> = Vec::new();
let mut list: Vec<List> = Vec::new();
let mut quote: Vec<Quote> = Vec::new();
// Define preformatted buffer
let mut code_multiline_buffer: Option<Multiline> = None;
let mut code_buffer: Option<Code> = 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);