Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 11 additions & 18 deletions .github/workflows/check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,22 @@ name: Check ✅
on:
workflow_dispatch:
push:
paths:
- "*"
pull_request:
paths:
- "*"
env:
CARGO_TERM_COLOR: always
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛒
uses: actions/checkout@v3
- name: Toolchain 🧰
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
components: rustfmt, clippy
override: true
- name: Check ✅
uses: actions-rs/cargo@v1
continue-on-error: false
with:
command: check
args: --verbose
uses: actions/checkout@v7
- name: Test 🧪
run: |
cargo test
cargo test --all-features
cargo test --no-default-features
for feature in ast blocking convert macros meta quick request; do
cargo test --no-default-features --features "$feature"
done
- name: Clippy 📎
run: cargo clippy --all-features --all-targets -- -D warnings
48 changes: 41 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[package]
name = "germ"
version = "0.4.8"
version = "0.5.0"
authors = ["Fuwn <contact@fuwn.me>"]
edition = "2021"
description = "The Ultimate Gemini Toolkit."
Expand All @@ -17,25 +17,59 @@ rust-version = "1.83.0"

[features]
ast = []
blocking = ["rustls", "url", "anyhow"]
blocking = ["rustls", "webpki-roots", "url", "anyhow"]
convert = ["ast"]
default = ["ast", "convert", "meta", "request"]
macros = ["ast", "convert"]
meta = []
request = ["rustls", "url", "anyhow", "tokio", "tokio-rustls"]
request = ["rustls", "webpki-roots", "url", "anyhow", "tokio", "tokio-rustls"]
quick = []
example-gemtext = []

[[example]]
name = "ast"
required-features = ["ast"]

[[example]]
name = "ast_to_gemtext"
required-features = ["ast"]

[[example]]
name = "html"
required-features = ["convert"]

[[example]]
name = "markdown"
required-features = ["convert"]

[[example]]
name = "meta"
required-features = ["meta"]

[[example]]
name = "request"
required-features = ["request"]

[[example]]
name = "request_blocking"
required-features = ["blocking"]

[[example]]
name = "request_blocking_to_gemtext_from_ast"
required-features = ["ast", "blocking"]

[dependencies]
anyhow = { version = "1.0.98", optional = true } # `Result`
rustls = { version = "0.21.12", features = [
"dangerous_configuration",
], optional = true } # TLS
rustls = { version = "0.21.12", optional = true, features = ["dangerous_configuration"] } # TLS
webpki-roots = { version = "1.0.9", optional = true } # CA certificates
tokio-rustls = { version = "0.24.1", optional = true } # Non-blocking TLS
tokio = { version = "1.45.1", optional = true, default-features = false, features = [
"net",
"io-util",
"rt-multi-thread",
"macros",
"time",
] } # Non-blocking I/O
url = { version = "2.5.4", optional = true } # URL Validation

[dev-dependencies]
rcgen = "=0.12.1"
111 changes: 73 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,23 @@
[![docs.rs](https://docs.rs/germ/badge.svg)](https://docs.rs/germ)
[![github.com](https://github.com/gemrest/germ/actions/workflows/check.yaml/badge.svg?branch=main)](https://github.com/gemrest/germ/actions/workflows/check.yaml)

The Ultimate Gemini Toolkit

Germ is a toolkit for the Gemini protocol which aims to have a little something
for everyone. At the moment, Germ has **ZERO** dependencies (unless you use the
`request` feature), and Germ will continue to try its hardest to have as few
dependencies as possible.

## Features

- AST builder to easily construct and manipulate AST trees from raw Gemtext
- Converters to easily convert from Gemtext to markup formats such as HTML or
Markdown
- Blocking and non-blocking request suite
- Structured meta section manipulation
- And more!

Check out the rest of the features in the Features section under Usage
Germ parses and generates Gemtext, converts it to HTML or Markdown, handles
response metadata, and makes Gemini requests. Its default features include
the request client and its network and TLS dependencies. Disable default
features to use `ast`, `convert`, `meta`, or `quick` without normal dependencies.

## Usage

Current version:
[![crates.io](https://img.shields.io/crates/v/germ.svg)](https://crates.io/crates/germ)

```toml
# Cargo.toml
[dependencies]
germ = "0.5.0"
```

To select features without the default request client:

```toml
[dependencies]
# To enable only the base (default) features: ast, convert, meta, request
# germ = "*" # Use current version show above!

# To enable only certain features
[dependencies.germ]
version = "*" # Use current version show above!
default-features = false
features = ["ast"] # Enable the features you would like to use!
germ = { version = "0.5.0", default-features = false, features = ["ast"] }
```

### Features
Expand All @@ -47,20 +29,73 @@ features = ["ast"] # Enable the features you would like to use!
| ---------- | --------------------------------------------------------------------- |
| `default` | `ast`, `convert`, `meta`, `request` |
| `ast` | Construct AST trees from raw Gemtext |
| `blocking` | Blocking equivalent of `request` |
| `convert` | Convert Gemtext to markup formats such as HTML or Markdown |
| `request` | Make Gemini requests, get sane, structured results |
| `meta` | Structure-ise a Gemini response's meta section |
| `macros` | Macros to aid with various Germ-related functionalities |
| `quick` | Tiny functions to create valid Gemtext elements from structured input |
| `blocking` | Make blocking Gemini requests |
| `convert` | Convert Gemtext to HTML or Markdown |
| `request` | Make asynchronous Gemini requests |
| `meta` | Parse and format Gemini response metadata |
| `macros` | Generate AST and converted output with macros |
| `quick` | Build individual Gemtext lines |

Gemini requests verify server certificates against Mozilla CA roots by default.
For a self-signed capsule, add its certificate in DER form to a
`rustls::RootCertStore`, then pass that store to
`request::blocking::request_with_roots` or
`request::non_blocking::request_with_roots`. The store is a trust anchor, not an
exact certificate fingerprint pin. Save and review the certificate separately
before trusting it; the default request functions do not remember certificates
between calls.

Requests time out after 30 seconds and have a 16 MiB response limit, including
the header. To change these limits or the trusted roots, set the fields of
`request::RequestOptions` and pass it to
`request::blocking::request_with_options` or
`request::non_blocking::request_with_options`. The blocking system DNS lookup
may outlast the timeout before it returns.

For trust on first use, implement `request::CertificateStore` with durable
storage keyed by hostname and port, then pass it to
`blocking::request_with_tofu` or `non_blocking::request_with_tofu` alongside
`RequestOptions`. These methods verify the presented certificate and complete
the TLS handshake before saving a first-use certificate or checking an existing
one, and before sending the URL. They reject an unexpected certificate change
until the stored certificate expires. An expired certificate is replaced after
the new one passes validation. The store must persist each update before
returning; callers can remove a pin deliberately when changing trust early.
Store methods run synchronously, including in the async request path.
TOFU uses the timeout and response size limits from `RequestOptions` but
ignores its `root_certificates` field.

`Ast::to_gemtext()` serialises parsed nodes and retains a consistent LF or CRLF
style and a final line ending. Mixed line endings and optional spaces after
Gemtext markers can be normalised, so exact source round trips are not
guaranteed.

HTML conversion escapes Gemtext content. HTML and Markdown conversion emit
links for relative targets and Gemini, Gopher, HTTP, HTTPS, mailto, and FTP
URLs; other schemes are shown as text. Markdown conversion escapes literal
text, link labels, and destinations, and chooses a code fence that does not
close inside preformatted content.

`Meta` parses quoted MIME parameters and quotes values as needed when formatting
them.

`quick::link(url, label)` takes the URL first and an optional label second.

Unknown Gemini status codes retain their numeric value. Use
`Status::category()` to inspect the response category indicated by the first
digit.

### Examples

Thoroughly commented examples can be found within the
Runnable examples can be found within the
[`examples/`](https://github.com/gemrest/germ/tree/main/examples) directory.

Examples can be run by name using the example just task.
(e.g., `just example ast_to_gemtext`)
Run one with `just example ast_to_gemtext`.

`just diff` fetches `gemini://geminiprotocol.net/` with a process-local TOFU
certificate store. The certificate pin is not retained between runs. The
example reports source formatting changes and checks that serialising and
reparsing preserves the parsed nodes.

## License

Expand Down
8 changes: 2 additions & 6 deletions examples/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,9 @@
//! abstract syntax tree.

fn main() {
// Parse `EXAMPLE_GEMTEXT` into an abstract syntax tree
let ast = germ::ast::Ast::from_string(germ::EXAMPLE_GEMTEXT);
// Get the nodes of the abstract syntax tree
let ast_nodes = ast.inner();
let ast = germ::ast::Ast::from_string(include_str!("example.gmi"));

// Print the abstract syntax tree nodes, one-by-one
for node in ast_nodes {
for node in ast.inner() {
println!("{:?}", node);
}
}
10 changes: 3 additions & 7 deletions examples/ast_to_gemtext.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
//! This example converts Gemtext into an abstract syntax tree and then back
//! into Gemtext, demonstrating both Germ's parsing and generation capabilities.
//! into Gemtext.

fn main() {
// Parse `EXAMPLE_GEMTEXT` into an abstract syntax tree
let ast = germ::ast::Ast::from_string(germ::EXAMPLE_GEMTEXT);
// Convert the abstract syntax tree back to Gemtext
let gemtext = ast.to_gemtext();
let ast = germ::ast::Ast::from_string(include_str!("example.gmi"));

// Print the Gemtext, identical to `EXAMPLE_GEMTEXT`
println!("{gemtext}");
print!("{}", ast.to_gemtext());
}
6 changes: 0 additions & 6 deletions examples/convert.html

This file was deleted.

28 changes: 0 additions & 28 deletions examples/convert.md

This file was deleted.

42 changes: 42 additions & 0 deletions examples/example.gmi
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
```This is alt-text
Here goes the pre-formatted text.

This continues the pre-formatted text on a new line after a blank line.
```

# This is a heading

This is some text.

This is more text after a blank line.

* This is a single list item.
* This is the next list item.

* This is a new list.
* This is the next item on the new list.

## This is a sub-heading

> This is a blockquote.

### This is a sub-sub-heading.

=> gemini://gem.rest/ This is a link to GemRest
=> /somewhere

```This is a preformatted block containing inner Gemtext.
=> gemini://fuwn.me/ This is a link.

* This is a list item.

> This is a blockquote.

# This is a heading.

## This is a sub-heading.

### This is a sub-sub-heading.
```

That was a link without text.
7 changes: 2 additions & 5 deletions examples/html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,10 @@
//! HTML.

fn main() {
// Convert the Gemtext to HTML
let html = germ::convert::from_string(
germ::EXAMPLE_GEMTEXT,
include_str!("example.gmi"),
&germ::convert::Target::HTML,
);

// Write the HTML to a file
std::fs::write("examples/convert.html", html)
.expect("could not write to file");
println!("{html}");
}
8 changes: 3 additions & 5 deletions examples/markdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@
//! Markdown.

fn main() {
// Convert the Gemtext to Markdown
let html = germ::convert::from_string(
germ::EXAMPLE_GEMTEXT,
let markdown = germ::convert::from_string(
include_str!("example.gmi"),
&germ::convert::Target::Markdown,
);

// Write the Markdown to a file
std::fs::write("examples/convert.md", html).expect("could not write to file");
println!("{markdown}");
}
Loading
Loading