mirror of
https://github.com/YGGverse/ggemtext.git
synced 2026-03-31 17:15:33 +00:00
Compare commits
95 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a51b67387 | ||
|
|
f83d049c60 | ||
|
|
2d98b66d82 | ||
|
|
459626acb4 | ||
|
|
ff70e5410a | ||
|
|
678f906f48 | ||
|
|
10ff400d8f | ||
|
|
25a45337ff | ||
|
|
841ee2036e | ||
|
|
22a05a975c | ||
|
|
0c90bbafba | ||
|
|
0c6ba0c87c | ||
|
|
92e1557c9c | ||
|
|
96f7d648b6 | ||
|
|
7f3ea670f1 | ||
|
|
83ec663929 | ||
|
|
7345400172 | ||
|
|
8eaad1dac9 | ||
|
|
a5638fde33 | ||
|
|
d72575fdc5 | ||
|
|
eedd7a73ff | ||
|
|
039b1db935 | ||
|
|
f550041b55 | ||
|
|
9392b39327 | ||
|
|
3e16995e00 | ||
|
|
c29f1ba529 | ||
|
|
5b751e3c7a | ||
|
|
7802869d0d | ||
|
|
9d27cdfb49 | ||
|
|
bf4ac4bd27 | ||
|
|
1b43f6aeaf | ||
|
|
9696efa02d | ||
|
|
23b04f26ec | ||
|
|
4ce1b20bf7 | ||
|
|
bab4e03940 | ||
|
|
7826104978 | ||
|
|
bafdcda7be | ||
|
|
085ec164b8 | ||
|
|
4d2c05c428 | ||
|
|
638d3cff08 | ||
|
|
2870aeb3fb | ||
|
|
0364760a35 | ||
|
|
094a404cf0 | ||
|
|
ffcf8f9627 | ||
|
|
7c2051acaf | ||
|
|
b37a5f9061 | ||
|
|
f2fbced415 | ||
|
|
407c3e2e13 | ||
|
|
f3dc550c2e | ||
|
|
e1cb4f9b99 | ||
|
|
c4d92d8d1e | ||
|
|
4ea42809d7 | ||
|
|
9f7b85b523 | ||
|
|
8c5e806bdc | ||
|
|
a9fed67a71 | ||
|
|
8428841f95 | ||
|
|
3e30b79d85 | ||
|
|
eccf5da808 | ||
|
|
b4d9686db5 | ||
|
|
f08ae77b7c | ||
|
|
facb90dfa9 | ||
|
|
429502ce72 | ||
|
|
2716cc4f0e | ||
|
|
04120b2c44 | ||
|
|
12091be50a | ||
|
|
544851bbb7 | ||
|
|
84c72ae3a3 | ||
|
|
d739181a76 | ||
|
|
3b9c1b1b1c | ||
|
|
155247a2ea | ||
|
|
2e0a1ae3ef | ||
|
|
4741365154 | ||
|
|
c6f747fefd | ||
|
|
ec9b989d0a | ||
|
|
1c95f8ad08 | ||
|
|
2fd9327c88 | ||
|
|
7d6c049870 | ||
|
|
dbe080a4f1 | ||
|
|
737117b91f | ||
|
|
5002fc392b | ||
|
|
9b531bfd82 | ||
|
|
8c00d4bf89 | ||
|
|
7f6c459065 | ||
|
|
0c3fa09769 | ||
|
|
dfb23931e3 | ||
|
|
93c7f738b7 | ||
|
|
127741351e | ||
|
|
5611d7ee5e | ||
|
|
09ab2da495 | ||
|
|
83d88ef692 | ||
|
|
dc9e6a14dc | ||
|
|
be813cf643 | ||
|
|
f12d052b47 | ||
|
|
dbb443d361 | ||
|
|
65b051f23e |
19 changed files with 773 additions and 434 deletions
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
custom: https://yggverse.github.io/#donate
|
||||
31
.github/workflows/build.yml
vendored
Normal file
31
.github/workflows/build.yml
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTFLAGS: -Dwarnings
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run rustfmt
|
||||
run: cargo fmt --all -- --check
|
||||
- name: Update packages index
|
||||
run: sudo apt update
|
||||
- name: Install system packages
|
||||
run: sudo apt install -y libglib2.0-dev
|
||||
- name: Run clippy
|
||||
run: cargo clippy --all-targets
|
||||
- name: Build
|
||||
run: cargo build --verbose
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
||||
11
Cargo.toml
11
Cargo.toml
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "ggemtext"
|
||||
version = "0.1.2"
|
||||
edition = "2021"
|
||||
version = "0.7.0"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
description = "Glib-oriented Gemtext API"
|
||||
|
|
@ -15,6 +15,7 @@ categories = [
|
|||
]
|
||||
repository = "https://github.com/YGGverse/ggemtext"
|
||||
|
||||
[dependencies.gtk]
|
||||
package = "gtk4"
|
||||
version = "0.9.1"
|
||||
[dependencies.glib]
|
||||
package = "glib"
|
||||
version = "0.21.0"
|
||||
features = ["v2_66"]
|
||||
|
|
|
|||
155
README.md
155
README.md
|
|
@ -1,5 +1,9 @@
|
|||
# ggemtext
|
||||
|
||||

|
||||
[](https://docs.rs/ggemtext)
|
||||
[](https://crates.io/crates/ggemtext)
|
||||
|
||||
Glib-oriented [Gemtext](https://geminiprotocol.net/docs/gemtext.gmi) API
|
||||
|
||||
## Install
|
||||
|
|
@ -10,24 +14,12 @@ cargo add ggemtext
|
|||
|
||||
## Usage
|
||||
|
||||
* [Documentation](https://docs.rs/ggemtext/latest/)
|
||||
|
||||
### Line
|
||||
|
||||
Line parser, useful for [TextTag](https://docs.gtk.org/gtk4/class.TextTag.html) operations in [TextBuffer](https://docs.gtk.org/gtk4/class.TextBuffer.html) context.
|
||||
|
||||
**Connect dependencies**
|
||||
|
||||
``` rust
|
||||
use ggemtext::line::{
|
||||
code::Code,
|
||||
header::{Header, Level},
|
||||
link::Link,
|
||||
list::List,
|
||||
quote::Quote,
|
||||
};
|
||||
```
|
||||
|
||||
**Prepare document**
|
||||
|
||||
Iterate Gemtext lines to continue with [Line](#Line) API:
|
||||
|
||||
``` rust
|
||||
|
|
@ -38,87 +30,114 @@ for line in gemtext.lines() {
|
|||
|
||||
#### Code
|
||||
|
||||
##### Inline
|
||||
|
||||
``` rust
|
||||
match Code::inline_from("```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
|
||||
|
||||
##### Multiline
|
||||
|
||||
``` rust
|
||||
match Code::multiline_begin_from("```alt") {
|
||||
Some(mut multiline) => {
|
||||
Code::multiline_continue_from(&mut multiline, "line 1");
|
||||
Code::multiline_continue_from(&mut multiline, "line 2");
|
||||
Code::multiline_continue_from(&mut multiline, "```"); // complete
|
||||
|
||||
assert!(multiline.completed);
|
||||
assert_eq!(multiline.alt, Some("alt".into()));
|
||||
assert_eq!(multiline.buffer.len(), 3);
|
||||
assert!(code.is_completed);
|
||||
assert_eq!(code.alt, Some("alt".into()));
|
||||
assert_eq!(code.value.len(), 12 + 2); // +NL
|
||||
}
|
||||
None => assert!(false),
|
||||
};
|
||||
None => unreachable!(),
|
||||
}
|
||||
```
|
||||
|
||||
#### Header
|
||||
|
||||
**Struct**
|
||||
|
||||
``` rust
|
||||
match Header::from("# H1") {
|
||||
use ggemtext::line::{Header, header::Level};
|
||||
match Header::parse("# 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),
|
||||
}; // H1, H2, H3
|
||||
None => unreachable!(),
|
||||
} // 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
|
||||
match Link::from(
|
||||
"=> gemini://geminiprotocol.net 1965-01-19 Gemini",
|
||||
None, // absolute path given, base not wanted
|
||||
Some(>k::glib::TimeZone::local()),
|
||||
) {
|
||||
Some(link) => {
|
||||
// Alt
|
||||
assert_eq!(link.alt, Some("Gemini".into()));
|
||||
use ggemtext::line::Link;
|
||||
|
||||
// Date
|
||||
match link.timestamp {
|
||||
Some(timestamp) => {
|
||||
assert_eq!(timestamp.year(), 1965);
|
||||
assert_eq!(timestamp.month(), 01);
|
||||
assert_eq!(timestamp.day_of_month(), 19);
|
||||
}
|
||||
None => assert!(false),
|
||||
}
|
||||
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
|
||||
|
||||
**Struct**
|
||||
|
||||
``` rust
|
||||
match List::from("* Item") {
|
||||
use ggemtext::line::List;
|
||||
match List::parse("* Item") {
|
||||
Some(list) => assert_eq!(list.value, "Item"),
|
||||
None => assert!(false),
|
||||
};
|
||||
None => unreachable!(),
|
||||
}
|
||||
```
|
||||
|
||||
**Trait**
|
||||
|
||||
``` rust
|
||||
use ggemtext::line::list::Gemtext;
|
||||
assert_eq!("* Item".as_value(), Some("Item"))
|
||||
assert_eq!("Item".to_source(), "* Item")
|
||||
```
|
||||
|
||||
#### Quote
|
||||
|
||||
**Struct**
|
||||
|
||||
``` rust
|
||||
match Quote::from("> Quote") {
|
||||
use ggemtext::line::Quote;
|
||||
match Quote::parse("> Quote") {
|
||||
Some(quote) => assert_eq!(quote.value, "Quote"),
|
||||
None => assert!(false),
|
||||
};
|
||||
```
|
||||
None => unreachable!(),
|
||||
}
|
||||
```
|
||||
|
||||
**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
|
||||
|
||||
## See also
|
||||
|
||||
* [ggemini](https://github.com/YGGverse/ggemini) - Glib-oriented client for Gemini Protocol
|
||||
|
|
@ -3,3 +3,9 @@ pub mod header;
|
|||
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;
|
||||
pub use quote::Quote;
|
||||
|
|
|
|||
|
|
@ -1,25 +1,90 @@
|
|||
pub mod inline;
|
||||
pub mod multiline;
|
||||
pub mod error;
|
||||
pub use error::Error;
|
||||
|
||||
use inline::Inline;
|
||||
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 {
|
||||
// nothing yet..
|
||||
pub alt: Option<String>,
|
||||
pub value: String,
|
||||
pub is_completed: bool,
|
||||
}
|
||||
|
||||
impl Code {
|
||||
// Inline
|
||||
pub fn inline_from(line: &str) -> Option<Inline> {
|
||||
Inline::from(line)
|
||||
// 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
|
||||
}
|
||||
|
||||
// Multiline
|
||||
pub fn multiline_begin_from(line: &str) -> Option<Multiline> {
|
||||
Multiline::begin_from(line)
|
||||
/// Continue preformatted buffer from line string,
|
||||
/// set `completed` as True on close tag found
|
||||
pub fn continue_from(&mut self, line: &str) -> Result<(), Error> {
|
||||
// Make sure buffer not completed yet
|
||||
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(())
|
||||
}
|
||||
|
||||
pub fn multiline_continue_from(this: &mut Multiline, line: &str) {
|
||||
Multiline::continue_from(this, line)
|
||||
// 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!(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
src/line/code/error.rs
Normal file
16
src/line/code/error.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
use std::fmt::{Display, Formatter, Result};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Completed,
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
match self {
|
||||
Self::Completed => {
|
||||
write!(f, "Could not continue as completed!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
use gtk::glib::{GString, Regex, RegexCompileFlags, RegexMatchFlags};
|
||||
|
||||
pub struct Inline {
|
||||
pub value: GString,
|
||||
}
|
||||
|
||||
impl Inline {
|
||||
pub fn from(line: &str) -> Option<Self> {
|
||||
// Parse line
|
||||
let regex = Regex::split_simple(
|
||||
r"^`{3}([^`]*)`{3}$",
|
||||
line,
|
||||
RegexCompileFlags::DEFAULT,
|
||||
RegexMatchFlags::DEFAULT,
|
||||
);
|
||||
|
||||
// Detect value
|
||||
let value = regex.get(1)?;
|
||||
|
||||
if value.trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Result
|
||||
Some(Self {
|
||||
value: GString::from(value.as_str()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
use gtk::glib::GString;
|
||||
|
||||
pub struct Multiline {
|
||||
pub alt: Option<GString>,
|
||||
pub buffer: Vec<GString>,
|
||||
pub completed: bool,
|
||||
}
|
||||
|
||||
impl Multiline {
|
||||
/// Search in line for tag open,
|
||||
/// return Self constructed on success or None
|
||||
pub fn begin_from(line: &str) -> Option<Self> {
|
||||
if line.starts_with("```") {
|
||||
let alt = line.trim_start_matches("```");
|
||||
|
||||
return Some(Self {
|
||||
alt: match alt.trim().is_empty() {
|
||||
true => None,
|
||||
false => Some(GString::from(alt)),
|
||||
},
|
||||
buffer: Vec::new(),
|
||||
completed: false,
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Continue preformatted buffer from line,
|
||||
/// set `completed` as True on close tag found
|
||||
pub fn continue_from(&mut self, line: &str) {
|
||||
// Make sure buffer not completed yet
|
||||
if self.completed {
|
||||
panic!("Could not continue as completed") // @TODO handle
|
||||
}
|
||||
|
||||
// Line contain close tag
|
||||
if line.ends_with("```") {
|
||||
self.completed = true;
|
||||
}
|
||||
|
||||
// Append data to the buffer, trim close tag on exists
|
||||
self.buffer
|
||||
.push(GString::from(line.trim_end_matches("```")));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,47 +1,76 @@
|
|||
use gtk::glib::{GString, Regex, RegexCompileFlags, RegexMatchFlags};
|
||||
pub mod gemtext;
|
||||
pub mod level;
|
||||
|
||||
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 {
|
||||
pub value: GString,
|
||||
pub level: Level,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
impl Header {
|
||||
pub fn from(line: &str) -> Option<Self> {
|
||||
// Parse line
|
||||
let regex = Regex::split_simple(
|
||||
r"^(#{1,3})\s*(.+)$",
|
||||
line,
|
||||
RegexCompileFlags::DEFAULT,
|
||||
RegexMatchFlags::DEFAULT,
|
||||
);
|
||||
// Constructors
|
||||
|
||||
// Detect header level
|
||||
let level = regex.get(1)?;
|
||||
|
||||
let level = match level.len() {
|
||||
1 => Level::H1,
|
||||
2 => Level::H2,
|
||||
3 => Level::H3,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
// Detect header value
|
||||
let value = regex.get(2)?;
|
||||
|
||||
if value.trim().is_empty() {
|
||||
return None;
|
||||
/// Parse `Self` from line string
|
||||
pub fn parse(line: &str) -> Option<Self> {
|
||||
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
|
||||
}
|
||||
|
||||
// Result
|
||||
Some(Self {
|
||||
level,
|
||||
value: GString::from(value.as_str()),
|
||||
})
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
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!(header.level.as_tag(), level.as_tag());
|
||||
assert_eq!(f(&header.to_source()), f(source));
|
||||
}
|
||||
// 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", Level::H2);
|
||||
test("## H2 ", "H2", Level::H2);
|
||||
test("##H2", "H2", Level::H2);
|
||||
test("##H2 ", "H2", Level::H2);
|
||||
// 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())
|
||||
}
|
||||
|
|
|
|||
98
src/line/header/gemtext.rs
Normal file
98
src/line/header/gemtext.rs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
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<Level>;
|
||||
/// 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> {
|
||||
self.strip_prefix(level.as_tag())
|
||||
.map(|postfix| postfix.trim())
|
||||
.filter(|value| !value.starts_with(Level::H1.as_tag()))
|
||||
}
|
||||
fn to_level(&self) -> Option<Level> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
#[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(l: &Level) {
|
||||
assert_eq!(VALUE.to_source(l), format!("{} {VALUE}", l.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);
|
||||
}
|
||||
16
src/line/header/level.rs
Normal file
16
src/line/header/level.rs
Normal file
|
|
@ -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 => "###",
|
||||
}
|
||||
}
|
||||
}
|
||||
188
src/line/link.rs
188
src/line/link.rs
|
|
@ -1,91 +1,119 @@
|
|||
use gtk::glib::{
|
||||
DateTime, GString, 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<GString>, // [optional] alternative link description
|
||||
pub is_external: Option<bool>, // [optional] external link indication, on base option provided
|
||||
pub timestamp: Option<DateTime>, // [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<String>,
|
||||
/// 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 {
|
||||
pub fn from(line: &str, base: Option<&Uri>, timezone: Option<&TimeZone>) -> Option<Self> {
|
||||
// Define initial values
|
||||
let mut alt = None;
|
||||
let mut timestamp = None;
|
||||
let mut is_external = None;
|
||||
// Constructors
|
||||
|
||||
// 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 unresolved_address = regex.get(1)?;
|
||||
|
||||
// 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) => {
|
||||
// Change external status
|
||||
is_external = Some(resolved_uri.scheme() != base_uri.scheme());
|
||||
|
||||
// Result
|
||||
resolved_uri
|
||||
}
|
||||
Err(_) => return None,
|
||||
}
|
||||
}
|
||||
Err(_) => return None,
|
||||
}
|
||||
}
|
||||
// Base resolve not requested
|
||||
None => {
|
||||
// Just 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) {
|
||||
// @TODO even possible, but simpler to work with `DateTime` API
|
||||
// await for new features in `Date` as better in Gemini context
|
||||
// https://docs.gtk.org/glib/struct.Date.html
|
||||
timestamp = match DateTime::from_iso8601(&format!("{date}T00:00:00"), timezone) {
|
||||
Ok(value) => Some(value),
|
||||
Err(_) => None,
|
||||
}
|
||||
/// Parse `Self` from line string
|
||||
pub fn parse(line: &str) -> Option<Self> {
|
||||
let l = line.strip_prefix(TAG)?.trim();
|
||||
let u = l.find(S).map_or(l, |i| &l[..i]);
|
||||
if u.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Alt
|
||||
if let Some(value) = regex.get(3) {
|
||||
if !value.is_empty() {
|
||||
alt = Some(GString::from(value.as_str()))
|
||||
}
|
||||
};
|
||||
|
||||
Some(Self {
|
||||
alt,
|
||||
is_external,
|
||||
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.trim());
|
||||
if let Some(ref alt) = self.alt {
|
||||
s.push(S);
|
||||
s.push_str(alt.trim());
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
||||
/// Get valid [DateTime](https://docs.gtk.org/glib/struct.DateTime.html) for `Self`
|
||||
pub fn time(&self, timezone: Option<&TimeZone>) -> Option<DateTime> {
|
||||
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<Uri> {
|
||||
// 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,38 @@
|
|||
use gtk::glib::{GString, Regex, RegexCompileFlags, RegexMatchFlags};
|
||||
pub mod gemtext;
|
||||
pub use gemtext::Gemtext;
|
||||
|
||||
/// [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 {
|
||||
pub value: GString,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
impl List {
|
||||
pub fn from(line: &str) -> Option<Self> {
|
||||
// Parse line
|
||||
let regex = Regex::split_simple(
|
||||
r"^\*\s*(.+)$",
|
||||
line,
|
||||
RegexCompileFlags::DEFAULT,
|
||||
RegexMatchFlags::DEFAULT,
|
||||
);
|
||||
// Constructors
|
||||
|
||||
// Detect value
|
||||
let value = regex.get(1)?;
|
||||
|
||||
if value.trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Result
|
||||
/// Parse `Self` from [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) line
|
||||
pub fn parse(line: &str) -> Option<Self> {
|
||||
Some(Self {
|
||||
value: GString::from(value.as_str()),
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
const SOURCE: &str = "* Item";
|
||||
const VALUE: &str = "Item";
|
||||
|
||||
let list = List::parse(SOURCE).unwrap();
|
||||
assert_eq!(list.value, VALUE);
|
||||
assert_eq!(list.to_source(), SOURCE);
|
||||
}
|
||||
|
|
|
|||
26
src/line/list/gemtext.rs
Normal file
26
src/line/list/gemtext.rs
Normal file
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -1,29 +1,39 @@
|
|||
use gtk::glib::{GString, Regex, RegexCompileFlags, RegexMatchFlags};
|
||||
pub mod gemtext;
|
||||
pub use gemtext::Gemtext;
|
||||
|
||||
/// [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 {
|
||||
pub value: GString,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
impl Quote {
|
||||
pub fn from(line: &str) -> Option<Self> {
|
||||
// Parse line
|
||||
let regex = Regex::split_simple(
|
||||
r"^>\s*(.+)$",
|
||||
line,
|
||||
RegexCompileFlags::DEFAULT,
|
||||
RegexMatchFlags::DEFAULT,
|
||||
);
|
||||
// Constructors
|
||||
|
||||
// Detect value
|
||||
let value = regex.get(1)?;
|
||||
|
||||
if value.trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Result
|
||||
/// Parse `Self` from [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) line
|
||||
pub fn parse(line: &str) -> Option<Self> {
|
||||
Some(Self {
|
||||
value: GString::from(value.as_str()),
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
const SOURCE: &str = "> Quote";
|
||||
const VALUE: &str = "Quote";
|
||||
|
||||
let quote = Quote::parse(SOURCE).unwrap();
|
||||
|
||||
assert_eq!(quote.value, VALUE);
|
||||
assert_eq!(quote.to_source(), SOURCE);
|
||||
}
|
||||
|
|
|
|||
26
src/line/quote/gemtext.rs
Normal file
26
src/line/quote/gemtext.rs
Normal file
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -7,12 +7,14 @@
|
|||
=> gemini://geminiprotocol.net Gemini
|
||||
=> gemini://geminiprotocol.net 1965-01-19 Gemini
|
||||
=> /docs/gemtext.gmi 1965-01-19 Gemini
|
||||
=> //:geminiprotocol.net
|
||||
=> //geminiprotocol.net
|
||||
=> //geminiprotocol.net/path
|
||||
=> //
|
||||
|
||||
* Listing item 1
|
||||
* Listing item 2
|
||||
|
||||
```inline code```
|
||||
|
||||
``` alt text
|
||||
multi
|
||||
preformatted line
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
use ggemtext::line::{
|
||||
code::{inline::Inline, multiline::Multiline, Code},
|
||||
Code, Link, List, Quote,
|
||||
header::{Header, Level},
|
||||
link::Link,
|
||||
list::List,
|
||||
quote::Quote,
|
||||
};
|
||||
|
||||
use gtk::glib::{TimeZone, Uri, UriFlags};
|
||||
use glib::{TimeZone, Uri, UriFlags};
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
|
|
@ -14,204 +11,238 @@ 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 header: Vec<Header> = Vec::new();
|
||||
let mut link: Vec<Link> = 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 = 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() {
|
||||
// Inline code
|
||||
if let Some(result) = Code::inline_from(line) {
|
||||
code_inline.push(result);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Multiline code
|
||||
match code_multiline_buffer {
|
||||
match code_buffer {
|
||||
None => {
|
||||
if let Some(code) = 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) => {
|
||||
Code::multiline_continue_from(result, line);
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
// Header
|
||||
if let Some(result) = Header::from(line) {
|
||||
header.push(result);
|
||||
if let Some(result) = Header::parse(line) {
|
||||
headers.push(result);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Link
|
||||
if let Some(result) = Link::from(line, base.as_ref(), timezone.as_ref()) {
|
||||
link.push(result);
|
||||
if let Some(result) = Link::parse(line) {
|
||||
links.push(result);
|
||||
continue;
|
||||
}
|
||||
|
||||
// List
|
||||
if let Some(result) = List::from(line) {
|
||||
if let Some(result) = List::parse(line) {
|
||||
list.push(result);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Quote
|
||||
if let Some(result) = Quote::from(line) {
|
||||
if let Some(result) = Quote::parse(line) {
|
||||
quote.push(result);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate inline code
|
||||
assert_eq!(code_inline.len(), 1);
|
||||
assert_eq!(code_inline.get(0).unwrap().value, "inline code");
|
||||
|
||||
// Validate multiline code
|
||||
assert_eq!(code_multiline.len(), 2);
|
||||
|
||||
// Validate code
|
||||
assert_eq!(code.len(), 2);
|
||||
{
|
||||
let item = code_multiline.get(0).unwrap();
|
||||
assert_eq!(item.alt.clone().unwrap(), " alt text");
|
||||
assert_eq!(item.buffer.len(), 3);
|
||||
assert_eq!(item.buffer.get(0).unwrap(), "multi");
|
||||
assert_eq!(item.buffer.get(1).unwrap(), " preformatted line");
|
||||
let item = code.first().unwrap();
|
||||
assert_eq!(item.alt.clone().unwrap(), "alt text");
|
||||
|
||||
assert_eq!(item.value.lines().count(), 2);
|
||||
|
||||
let mut lines = item.value.lines();
|
||||
assert_eq!(lines.next().unwrap(), "multi");
|
||||
assert_eq!(lines.next().unwrap(), " preformatted line");
|
||||
} // #1
|
||||
|
||||
{
|
||||
let item = code_multiline.get(1).unwrap();
|
||||
let item = code.get(1).unwrap();
|
||||
assert_eq!(item.alt.clone(), None);
|
||||
assert_eq!(item.buffer.len(), 3);
|
||||
assert_eq!(item.buffer.get(0).unwrap(), "alt-less");
|
||||
assert_eq!(item.buffer.get(1).unwrap(), " preformatted line");
|
||||
|
||||
assert_eq!(item.value.lines().count(), 2);
|
||||
|
||||
let mut lines = item.value.lines();
|
||||
assert_eq!(lines.next().unwrap(), "alt-less");
|
||||
assert_eq!(lines.next().unwrap(), " preformatted line");
|
||||
} // #2
|
||||
|
||||
// Validate headers
|
||||
assert_eq!(header.len(), 3);
|
||||
assert_eq!(headers.len(), 3);
|
||||
|
||||
fn to_i8(level: &Level) -> i8 {
|
||||
fn to_u8(level: &Level) -> u8 {
|
||||
match level {
|
||||
Level::H1 => 1,
|
||||
Level::H2 => 2,
|
||||
Level::H3 => 3,
|
||||
}
|
||||
} // comparison helper
|
||||
|
||||
let mut header = headers.iter();
|
||||
{
|
||||
let item = header.get(0).unwrap();
|
||||
let item = header.next().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();
|
||||
let item = header.next().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();
|
||||
let item = header.next().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
|
||||
|
||||
// Validate links
|
||||
assert_eq!(link.len(), 5);
|
||||
|
||||
assert_eq!(links.len(), 9);
|
||||
let mut link = links.iter();
|
||||
{
|
||||
let item = link.get(0).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"); // @TODO len 27?
|
||||
assert_eq!(item.time(Some(&timezone)), None);
|
||||
assert_eq!(
|
||||
item.uri(Some(&base)).unwrap().to_str(),
|
||||
"gemini://geminiprotocol.net"
|
||||
);
|
||||
} // #1
|
||||
|
||||
{
|
||||
let item = link.get(1).unwrap();
|
||||
|
||||
assert_eq!(item.alt, None);
|
||||
|
||||
let timestamp = item.timestamp.clone().unwrap();
|
||||
assert_eq!(timestamp.year(), 1965);
|
||||
assert_eq!(timestamp.month(), 01);
|
||||
assert_eq!(timestamp.day_of_month(), 19);
|
||||
|
||||
assert_eq!(item.uri.to_str(), "gemini://geminiprotocol.net");
|
||||
} // #2
|
||||
|
||||
{
|
||||
let item = link.get(2).unwrap();
|
||||
|
||||
assert_eq!(item.alt.clone().unwrap(), "Gemini");
|
||||
assert_eq!(item.timestamp, None);
|
||||
assert_eq!(item.uri.to_str(), "gemini://geminiprotocol.net");
|
||||
} // #3
|
||||
|
||||
{
|
||||
let item = link.get(3).unwrap();
|
||||
|
||||
assert_eq!(item.alt.clone().unwrap(), "Gemini");
|
||||
|
||||
let timestamp = item.timestamp.clone().unwrap();
|
||||
assert_eq!(timestamp.year(), 1965);
|
||||
assert_eq!(timestamp.month(), 01);
|
||||
assert_eq!(timestamp.day_of_month(), 19);
|
||||
|
||||
assert_eq!(item.uri.to_str(), "gemini://geminiprotocol.net");
|
||||
} // #4
|
||||
|
||||
{
|
||||
let item = link.get(4).unwrap();
|
||||
|
||||
assert_eq!(item.alt.clone().unwrap(), "Gemini");
|
||||
|
||||
let timestamp = item.timestamp.clone().unwrap();
|
||||
assert_eq!(timestamp.year(), 1965);
|
||||
assert_eq!(timestamp.month(), 01);
|
||||
assert_eq!(timestamp.day_of_month(), 19);
|
||||
let item = link.next().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
item.uri.to_str(),
|
||||
item.uri(Some(&base)).unwrap().to_string(),
|
||||
"gemini://geminiprotocol.net"
|
||||
);
|
||||
|
||||
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.alt, Some("1965-01-19".to_string()));
|
||||
} // #2
|
||||
{
|
||||
let item = link.next().unwrap();
|
||||
|
||||
assert_eq!(item.alt.clone().unwrap(), "Gemini");
|
||||
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, Some("1965-01-19 Gemini".to_string()));
|
||||
|
||||
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(Some(&base)).unwrap().to_string(),
|
||||
"gemini://geminiprotocol.net"
|
||||
);
|
||||
} // #4
|
||||
{
|
||||
let item = link.next().unwrap();
|
||||
|
||||
assert_eq!(item.alt, Some("1965-01-19 Gemini".to_string()));
|
||||
|
||||
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(Some(&base)).unwrap().to_string(),
|
||||
"gemini://geminiprotocol.net/docs/gemtext.gmi"
|
||||
);
|
||||
} // #5
|
||||
{
|
||||
let item = link.next().unwrap();
|
||||
|
||||
assert_eq!(item.alt, None);
|
||||
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.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.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.time(Some(&timezone)), None);
|
||||
assert_eq!(
|
||||
item.uri(Some(&base)).unwrap().to_string(),
|
||||
"gemini://geminiprotocol.net/"
|
||||
);
|
||||
} // #9
|
||||
|
||||
// Validate lists
|
||||
assert_eq!(list.len(), 2);
|
||||
assert_eq!(list.get(0).unwrap().value, "Listing item 1");
|
||||
assert_eq!(list.get(1).unwrap().value, "Listing item 2");
|
||||
assert_eq!(list.first().unwrap().value, "Listing item 1");
|
||||
assert_eq!(list.last().unwrap().value, "Listing item 2");
|
||||
|
||||
// Validate quotes
|
||||
assert_eq!(quote.len(), 1);
|
||||
assert_eq!(quote.get(0).unwrap().value, "quoted string");
|
||||
assert_eq!(quote.first().unwrap().value, "quoted string");
|
||||
}
|
||||
// Could not load gemtext file
|
||||
Err(_) => {
|
||||
assert!(false);
|
||||
}
|
||||
Err(_) => panic!(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue