mirror of
https://github.com/YGGverse/ggemtext.git
synced 2026-04-01 17:45:36 +00:00
Compare commits
No commits in common. "main" and "0.5.0" have entirely different histories.
10 changed files with 217 additions and 120 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "ggemtext"
|
name = "ggemtext"
|
||||||
version = "0.7.0"
|
version = "0.5.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
@ -17,5 +17,5 @@ repository = "https://github.com/YGGverse/ggemtext"
|
||||||
|
|
||||||
[dependencies.glib]
|
[dependencies.glib]
|
||||||
package = "glib"
|
package = "glib"
|
||||||
version = "0.21.0"
|
version = "0.20.9"
|
||||||
features = ["v2_66"]
|
features = ["v2_66"]
|
||||||
|
|
|
||||||
48
README.md
48
README.md
|
|
@ -28,21 +28,41 @@ for line in gemtext.lines() {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Code
|
#### Inline code
|
||||||
|
|
||||||
|
**Struct**
|
||||||
|
|
||||||
``` rust
|
``` rust
|
||||||
use ggemtext::line::Code;
|
use ggemtext::line::code::Inline;
|
||||||
match Code::begin_from("```alt") {
|
match Inline::parse("```inline```") {
|
||||||
Some(mut code) => {
|
Some(inline) => assert_eq!(inline.value, "inline"),
|
||||||
assert!(code.continue_from("line 1").is_ok());
|
None => assert!(false),
|
||||||
assert!(code.continue_from("line 2").is_ok());
|
}
|
||||||
assert!(code.continue_from("```").is_ok()); // complete
|
```
|
||||||
|
|
||||||
assert!(code.is_completed);
|
**Trait**
|
||||||
assert_eq!(code.alt, Some("alt".into()));
|
|
||||||
assert_eq!(code.value.len(), 12 + 2); // +NL
|
``` 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.level as u8, Level::H1 as u8);
|
||||||
assert_eq!(h1.value, "H1");
|
assert_eq!(h1.value, "H1");
|
||||||
}
|
}
|
||||||
None => unreachable!(),
|
None => assert!(false),
|
||||||
} // H1, H2, H3
|
} // H1, H2, H3
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -102,7 +122,7 @@ assert_eq!(link.to_source(), SOURCE);
|
||||||
use ggemtext::line::List;
|
use ggemtext::line::List;
|
||||||
match List::parse("* Item") {
|
match List::parse("* Item") {
|
||||||
Some(list) => assert_eq!(list.value, "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;
|
use ggemtext::line::Quote;
|
||||||
match Quote::parse("> Quote") {
|
match Quote::parse("> Quote") {
|
||||||
Some(quote) => assert_eq!(quote.value, "Quote"),
|
Some(quote) => assert_eq!(quote.value, "Quote"),
|
||||||
None => unreachable!(),
|
None => assert!(false),
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ pub mod link;
|
||||||
pub mod list;
|
pub mod list;
|
||||||
pub mod quote;
|
pub mod quote;
|
||||||
|
|
||||||
pub use code::Code;
|
|
||||||
pub use header::Header;
|
pub use header::Header;
|
||||||
pub use link::Link;
|
pub use link::Link;
|
||||||
pub use list::List;
|
pub use list::List;
|
||||||
|
|
|
||||||
|
|
@ -1,90 +1,7 @@
|
||||||
pub mod error;
|
pub mod inline;
|
||||||
pub use error::Error;
|
pub mod multiline;
|
||||||
|
|
||||||
|
pub use inline::Inline;
|
||||||
|
pub use multiline::Multiline;
|
||||||
|
|
||||||
pub const TAG: &str = "```";
|
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!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
46
src/line/code/inline.rs
Normal file
46
src/line/code/inline.rs
Normal file
|
|
@ -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<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());
|
||||||
|
}
|
||||||
38
src/line/code/inline/gemtext.rs
Normal file
38
src/line/code/inline/gemtext.rs
Normal file
|
|
@ -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());
|
||||||
|
}
|
||||||
59
src/line/code/multiline.rs
Normal file
59
src/line/code/multiline.rs
Normal file
|
|
@ -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<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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,8 @@
|
||||||
* Listing item 1
|
* Listing item 1
|
||||||
* Listing item 2
|
* Listing item 2
|
||||||
|
|
||||||
|
```inline code```
|
||||||
|
|
||||||
``` alt text
|
``` alt text
|
||||||
multi
|
multi
|
||||||
preformatted line
|
preformatted line
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
use ggemtext::line::{
|
use ggemtext::line::{
|
||||||
Code, Link, List, Quote,
|
code::{Inline, Multiline},
|
||||||
header::{Header, Level},
|
header::{Header, Level},
|
||||||
|
link::Link,
|
||||||
|
list::List,
|
||||||
|
quote::Quote,
|
||||||
};
|
};
|
||||||
|
|
||||||
use glib::{TimeZone, Uri, UriFlags};
|
use glib::{TimeZone, Uri, UriFlags};
|
||||||
|
|
@ -11,14 +14,15 @@ fn gemtext() {
|
||||||
match fs::read_to_string("tests/integration.gmi") {
|
match fs::read_to_string("tests/integration.gmi") {
|
||||||
Ok(gemtext) => {
|
Ok(gemtext) => {
|
||||||
// Init tags collection
|
// Init tags collection
|
||||||
let mut code: Vec<Code> = Vec::new();
|
let mut code_inline: Vec<Inline> = Vec::new();
|
||||||
|
let mut code_multiline: Vec<Multiline> = Vec::new();
|
||||||
let mut headers: Vec<Header> = Vec::new();
|
let mut headers: Vec<Header> = Vec::new();
|
||||||
let mut links: Vec<Link> = Vec::new();
|
let mut links: Vec<Link> = Vec::new();
|
||||||
let mut list: Vec<List> = Vec::new();
|
let mut list: Vec<List> = Vec::new();
|
||||||
let mut quote: Vec<Quote> = Vec::new();
|
let mut quote: Vec<Quote> = Vec::new();
|
||||||
|
|
||||||
// Define preformatted buffer
|
// Define preformatted buffer
|
||||||
let mut code_buffer: Option<Code> = None;
|
let mut code_multiline_buffer: Option<Multiline> = None;
|
||||||
|
|
||||||
// Define base URI as integration.gmi contain one relative link
|
// Define base URI as integration.gmi contain one relative link
|
||||||
let base = Uri::parse("gemini://geminiprotocol.net", UriFlags::NONE).unwrap();
|
let base = Uri::parse("gemini://geminiprotocol.net", UriFlags::NONE).unwrap();
|
||||||
|
|
@ -28,18 +32,25 @@ fn gemtext() {
|
||||||
|
|
||||||
// Parse document by line
|
// Parse document by line
|
||||||
for line in gemtext.lines() {
|
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 => {
|
None => {
|
||||||
if let Some(code) = Code::begin_from(line) {
|
if let Some(code) = Multiline::begin_from(line) {
|
||||||
code_buffer = Some(code);
|
code_multiline_buffer = Some(code);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(ref mut c) => {
|
Some(ref mut result) => {
|
||||||
assert!(c.continue_from(line).is_ok());
|
assert!(Multiline::continue_from(result, line).is_ok());
|
||||||
if c.is_completed {
|
if result.completed {
|
||||||
code.push(code_buffer.take().unwrap());
|
code_multiline.push(code_multiline_buffer.take().unwrap());
|
||||||
code_buffer = None;
|
code_multiline_buffer = None;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -70,10 +81,15 @@ fn gemtext() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate code
|
// Validate inline code
|
||||||
assert_eq!(code.len(), 2);
|
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.alt.clone().unwrap(), "alt text");
|
||||||
|
|
||||||
assert_eq!(item.value.lines().count(), 2);
|
assert_eq!(item.value.lines().count(), 2);
|
||||||
|
|
@ -84,7 +100,7 @@ fn gemtext() {
|
||||||
} // #1
|
} // #1
|
||||||
|
|
||||||
{
|
{
|
||||||
let item = code.get(1).unwrap();
|
let item = code_multiline.get(1).unwrap();
|
||||||
assert_eq!(item.alt.clone(), None);
|
assert_eq!(item.alt.clone(), None);
|
||||||
|
|
||||||
assert_eq!(item.value.lines().count(), 2);
|
assert_eq!(item.value.lines().count(), 2);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue