aboutsummaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/germ-macros-impl/Cargo.toml21
-rw-r--r--crates/germ-macros-impl/src/lib.rs58
-rw-r--r--crates/germ/Cargo.toml31
-rw-r--r--crates/germ/examples/ast.rs52
-rw-r--r--crates/germ/examples/ast_to_gemtext.rs53
-rw-r--r--crates/germ/examples/convert.html6
-rw-r--r--crates/germ/examples/convert.md28
-rw-r--r--crates/germ/examples/html.rs54
-rw-r--r--crates/germ/examples/markdown.rs57
-rw-r--r--crates/germ/examples/meta.rs24
-rw-r--r--crates/germ/examples/request.rs24
-rw-r--r--crates/germ/src/ast.rs28
-rw-r--r--crates/germ/src/ast/container.rs290
-rw-r--r--crates/germ/src/ast/macros.rs52
-rw-r--r--crates/germ/src/ast/node.rs173
-rw-r--r--crates/germ/src/convert.rs69
-rw-r--r--crates/germ/src/convert/html.rs75
-rw-r--r--crates/germ/src/convert/markdown.rs77
-rw-r--r--crates/germ/src/lib.rs43
-rw-r--r--crates/germ/src/meta.rs162
-rw-r--r--crates/germ/src/request.rs73
-rw-r--r--crates/germ/src/request/response.rs71
-rw-r--r--crates/germ/src/request/status.rs110
-rw-r--r--crates/germ/src/request/verifier.rs39
-rw-r--r--crates/germ/tests/ast.rs131
-rw-r--r--crates/germ/tests/convert.rs61
-rw-r--r--crates/germ/tests/meta.rs101
-rw-r--r--crates/germ/tests/status.rs32
28 files changed, 1995 insertions, 0 deletions
diff --git a/crates/germ-macros-impl/Cargo.toml b/crates/germ-macros-impl/Cargo.toml
new file mode 100644
index 0000000..18c41ac
--- /dev/null
+++ b/crates/germ-macros-impl/Cargo.toml
@@ -0,0 +1,21 @@
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[package]
+name = "germ-macros-impl"
+version = "0.2.8"
+authors = ["Fuwn <[email protected]>"]
+edition = "2021"
+description = "The Ultimate Gemini Toolkit."
+documentation = "https://docs.rs/germ"
+readme = "README.md"
+homepage = "https://github.com/gemrest/germ"
+repository = "https://github.com/gemrest/germ"
+license = "GPL-3.0-only"
+keywords = ["gemini", "parser", "lexer", "markdown", "converter"]
+categories = ["encoding"]
+
+[lib]
+proc-macro = true
+
+[dependencies]
+quote = "1.0.18" # Quasi-quoting
diff --git a/crates/germ-macros-impl/src/lib.rs b/crates/germ-macros-impl/src/lib.rs
new file mode 100644
index 0000000..30fdd81
--- /dev/null
+++ b/crates/germ-macros-impl/src/lib.rs
@@ -0,0 +1,58 @@
+// This file is part of Germ <https://github.com/gemrest/germ>.
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+//
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-only
+
+#![deny(
+ warnings,
+ nonstandard_style,
+ unused,
+ future_incompatible,
+ rust_2018_idioms,
+ unsafe_code,
+ clippy::all,
+ clippy::nursery,
+ clippy::pedantic
+)]
+#![feature(proc_macro_hygiene, proc_macro_span)]
+#![recursion_limit = "128"]
+
+use proc_macro::TokenStream;
+
+/// Convert Gemtext into a token tree
+///
+/// # Panics
+///
+/// May panic if the Gemini could not be properly handled, for any reason.
+#[proc_macro]
+pub fn gemini_to_tt(input: TokenStream) -> TokenStream {
+ let mut tokens = input.into_iter();
+ let mut span = tokens.next().unwrap().span();
+
+ for token in tokens {
+ span = span.join(token.span()).unwrap();
+ }
+
+ let gemini = span
+ .source_text()
+ .unwrap()
+ .lines()
+ .map(|l| l.trim_start().to_string())
+ .collect::<Vec<String>>()
+ .join("\n");
+
+ quote::quote!(#gemini).into()
+}
diff --git a/crates/germ/Cargo.toml b/crates/germ/Cargo.toml
new file mode 100644
index 0000000..105719d
--- /dev/null
+++ b/crates/germ/Cargo.toml
@@ -0,0 +1,31 @@
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[package]
+name = "germ"
+version = "0.2.8"
+authors = ["Fuwn <[email protected]>"]
+edition = "2021"
+description = "The Ultimate Gemini Toolkit."
+documentation = "https://docs.rs/germ"
+readme = "../../../README.md"
+homepage = "https://github.com/gemrest/germ"
+repository = "https://github.com/gemrest/germ"
+license = "GPL-3.0-only"
+keywords = ["gemini", "parser", "lexer", "markdown", "converter"]
+categories = ["encoding"]
+
+[features]
+ast = []
+convert = ["ast"]
+default = ["ast", "convert", "meta", "request"]
+macros = ["ast", "convert", "meta"]
+meta = []
+request = ["rustls", "url", "anyhow"]
+
+[dependencies]
+anyhow = { version = "1.0.57", optional = true } # `Result`
+germ-macros-impl = { path = "../germ-macros-impl" } # Germ's Macro Implementations
+rustls = { version = "0.20.5", features = [
+ "dangerous_configuration"
+], optional = true } # TLS
+url = { version = "2.2.2", optional = true } # URL Validation
diff --git a/crates/germ/examples/ast.rs b/crates/germ/examples/ast.rs
new file mode 100644
index 0000000..884bd91
--- /dev/null
+++ b/crates/germ/examples/ast.rs
@@ -0,0 +1,52 @@
+// This file is part of Germ <https://github.com/gemrest/germ>.
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+//
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-only
+
+const EXAMPLE_GEMTEXT: &str = r#"```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
+
+That was a link without text."#;
+
+fn main() {
+ for node in germ::ast::Ast::from_string(EXAMPLE_GEMTEXT).inner() {
+ println!("{:?}", node);
+ }
+}
diff --git a/crates/germ/examples/ast_to_gemtext.rs b/crates/germ/examples/ast_to_gemtext.rs
new file mode 100644
index 0000000..5ceef21
--- /dev/null
+++ b/crates/germ/examples/ast_to_gemtext.rs
@@ -0,0 +1,53 @@
+// This file is part of Germ <https://github.com/gemrest/germ>.
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+//
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-only
+
+const EXAMPLE_GEMTEXT: &str = r#"```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
+
+That was a link without text."#;
+
+fn main() {
+ println!(
+ "{}",
+ germ::ast::Ast::from_string(EXAMPLE_GEMTEXT).to_gemtext()
+ );
+}
diff --git a/crates/germ/examples/convert.html b/crates/germ/examples/convert.html
new file mode 100644
index 0000000..cdfde66
--- /dev/null
+++ b/crates/germ/examples/convert.html
@@ -0,0 +1,6 @@
+<pre>Here goes the pre-formatted text.
+
+This continues the pre-formatted text on a new line after a blank line.
+</pre><h1>This is a heading</h1><p>This is some text.</p><p>This is more text after a blank line.</p><ul><li>This is a single list item.</li>
+<li>This is the next list item.</li></ul><ul><li>This is a new list.</li>
+<li>This is the next item on the new list.</li></ul><h2>This is a sub-heading</h2><blockquote>This is a blockquote.</blockquote><h3>This is a sub-sub-heading.</h3><a href="gemini://gem.rest/">This is a link to GemRest</a><br><a href="/somewhere">/somewhere</a><br><p>That was a link without text.</p> \ No newline at end of file
diff --git a/crates/germ/examples/convert.md b/crates/germ/examples/convert.md
new file mode 100644
index 0000000..ea553ee
--- /dev/null
+++ b/crates/germ/examples/convert.md
@@ -0,0 +1,28 @@
+```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.
+
+[This is a link to GemRest](gemini://gem.rest/)
+</somewhere>
+
+That was a link without text.
diff --git a/crates/germ/examples/html.rs b/crates/germ/examples/html.rs
new file mode 100644
index 0000000..541c01e
--- /dev/null
+++ b/crates/germ/examples/html.rs
@@ -0,0 +1,54 @@
+// This file is part of Germ <https://github.com/gemrest/germ>.
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+//
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-only
+
+const EXAMPLE_GEMTEXT: &str = r#"```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
+
+That was a link without text."#;
+
+fn main() {
+ std::fs::write(
+ "examples/convert.html",
+ germ::convert::from_string(EXAMPLE_GEMTEXT, &germ::convert::Target::HTML),
+ )
+ .expect("could not write to file");
+}
diff --git a/crates/germ/examples/markdown.rs b/crates/germ/examples/markdown.rs
new file mode 100644
index 0000000..c14cdc5
--- /dev/null
+++ b/crates/germ/examples/markdown.rs
@@ -0,0 +1,57 @@
+// This file is part of Germ <https://github.com/gemrest/germ>.
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+//
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-only
+
+const EXAMPLE_GEMTEXT: &str = r#"```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
+
+That was a link without text."#;
+
+fn main() {
+ std::fs::write(
+ "examples/convert.md",
+ germ::convert::from_string(
+ EXAMPLE_GEMTEXT,
+ &germ::convert::Target::Markdown,
+ ),
+ )
+ .expect("could not write to file");
+}
diff --git a/crates/germ/examples/meta.rs b/crates/germ/examples/meta.rs
new file mode 100644
index 0000000..a9f4077
--- /dev/null
+++ b/crates/germ/examples/meta.rs
@@ -0,0 +1,24 @@
+// This file is part of Germ <https://github.com/gemrest/germ>.
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+//
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-only
+
+fn main() {
+ println!(
+ "{:?}",
+ germ::meta::Meta::from_string("text/gemini; hi=2; hi2=string=2")
+ )
+}
diff --git a/crates/germ/examples/request.rs b/crates/germ/examples/request.rs
new file mode 100644
index 0000000..e33710f
--- /dev/null
+++ b/crates/germ/examples/request.rs
@@ -0,0 +1,24 @@
+// This file is part of Germ <https://github.com/gemrest/germ>.
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+//
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-only
+
+fn main() {
+ match germ::request::request(&url::Url::parse("gemini://fuwn.me").unwrap()) {
+ Ok(response) => println!("{:?}", response),
+ Err(_) => {}
+ }
+}
diff --git a/crates/germ/src/ast.rs b/crates/germ/src/ast.rs
new file mode 100644
index 0000000..8c00b52
--- /dev/null
+++ b/crates/germ/src/ast.rs
@@ -0,0 +1,28 @@
+// This file is part of Germ <https://github.com/gemrest/germ>.
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+//
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-only
+
+//! Build AST trees from Gemtext
+
+mod container;
+mod node;
+
+#[cfg(feature = "macros")]
+mod macros;
+
+pub use container::Ast;
+pub use node::Node;
diff --git a/crates/germ/src/ast/container.rs b/crates/germ/src/ast/container.rs
new file mode 100644
index 0000000..d9e4d18
--- /dev/null
+++ b/crates/germ/src/ast/container.rs
@@ -0,0 +1,290 @@
+// This file is part of Germ <https://github.com/gemrest/germ>.
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+//
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-only
+
+use super::Node;
+
+/// An AST structure which contains an AST tree
+///
+/// # Example
+///
+/// ```rust
+/// let _ = germ::ast::Ast::from_string(r#"=> gemini://gem.rest/ GemRest"#);
+/// ```
+pub struct Ast {
+ inner: Vec<Node>,
+}
+impl Ast {
+ /// Build an AST tree from Gemtext.
+ ///
+ /// # Example
+ ///
+ /// ```rust
+ /// let _ = germ::ast::Ast::from_string(r#"=> gemini://gem.rest/ GemRest"#);
+ /// ```
+ #[must_use]
+ pub fn from_string(source: &str) -> Self {
+ let mut ast = vec![];
+ let mut in_preformatted = false;
+ let mut in_list = false;
+ let mut lines = source.lines();
+
+ // Iterate over all lines in the Gemtext `source`
+ while let Some(line) = lines.next() {
+ // Evaluate the Gemtext line and append its AST node to the `ast` tree
+ ast.append(&mut Self::evaluate(
+ line,
+ &mut lines,
+ &mut in_preformatted,
+ &mut in_list,
+ ));
+ }
+
+ Self {
+ inner: ast
+ }
+ }
+
+ #[must_use]
+ pub fn to_gemtext(&self) -> String {
+ let mut gemtext = "".to_string();
+
+ for node in &self.inner {
+ match node {
+ Node::Text(text) => gemtext.push_str(&format!("{}\n", text)),
+ Node::Link {
+ to,
+ text,
+ } =>
+ gemtext.push_str(&format!(
+ "=> {}{}\n",
+ to,
+ text
+ .clone()
+ .map_or_else(|| "".to_string(), |text| format!(" {}", text)),
+ )),
+ Node::Heading {
+ level,
+ text,
+ } =>
+ gemtext.push_str(&format!(
+ "{} {}\n",
+ match level {
+ 1 => "#",
+ 2 => "##",
+ 3 => "###",
+ _ => "",
+ },
+ text
+ )),
+ Node::List(items) =>
+ gemtext.push_str(&format!(
+ "{}\n",
+ items
+ .iter()
+ .map(|i| format!("* {}", i))
+ .collect::<Vec<String>>()
+ .join("\n"),
+ )),
+ Node::Blockquote(text) => gemtext.push_str(&format!("> {}\n", text)),
+ Node::PreformattedText {
+ alt_text,
+ text,
+ } =>
+ gemtext.push_str(&format!(
+ "```{}\n{}```\n",
+ alt_text.clone().unwrap_or_else(|| "".to_string()),
+ text
+ )),
+ Node::Whitespace => gemtext.push('\n'),
+ }
+ }
+
+ gemtext
+ }
+
+ /// The actual AST of `Ast`
+ ///
+ /// # Example
+ ///
+ /// ```rust
+ /// let _ =
+ /// germ::ast::Ast::from_string(r#"=> gemini://gem.rest/ GemRest"#).inner();
+ /// ```
+ #[must_use]
+ pub const fn inner(&self) -> &Vec<Node> { &self.inner }
+
+ #[allow(clippy::too_many_lines)]
+ fn evaluate(
+ line: &str,
+ lines: &mut std::str::Lines<'_>,
+ in_preformatted: &mut bool,
+ in_list: &mut bool,
+ ) -> Vec<Node> {
+ let mut preformatted = String::new();
+ let mut alt_text = String::new();
+ let mut nodes = vec![];
+ let mut line = line;
+ let mut list_items = vec![];
+
+ // Enter a not-so-infinite loop as sometimes, we may need to stay in an
+ // evaluation loop, e.g., multiline contexts: preformatted text, lists, etc.
+ loop {
+ // Match the first character of the Gemtext line to understand the line
+ // type
+ match line.get(0..1).unwrap_or("") {
+ "=" => {
+ // If the Gemtext line starts with an "=" ("=>"), it is a link line,
+ // so splitting it up should be easy enough.
+ let line = line.get(2..).unwrap();
+ let mut split = line
+ .split_whitespace()
+ .map(String::from)
+ .collect::<Vec<String>>()
+ .into_iter();
+
+ nodes.push(Node::Link {
+ to: split.next().expect("no location in link"),
+ text: {
+ let rest = split.collect::<Vec<String>>().join(" ");
+
+ if rest.is_empty() {
+ None
+ } else {
+ Some(rest)
+ }
+ },
+ });
+
+ break;
+ }
+ "#" => {
+ // If the Gemtext line starts with an "#", it is a heading, so let's
+ // find out how deep it goes.
+ let level = match line.get(0..3) {
+ Some(root) =>
+ if root.contains("###") {
+ 3
+ } else if root.contains("##") {
+ 2
+ } else if root.contains('#') {
+ 1
+ } else {
+ 0
+ },
+ None => 0,
+ };
+
+ nodes.push(Node::Heading {
+ level,
+ // Here, we are `get`ing the `&str` starting at the `level`-th
+ // index, then trimming the start. These operations
+ // effectively off the line identifier.
+ text: line.get(level..).unwrap_or("").trim_start().to_string(),
+ });
+
+ break;
+ }
+ "*" => {
+ // If the Gemtext line starts with an asterisk, it is a list item, so
+ // let's enter a list context.
+ if !*in_list {
+ *in_list = true;
+ }
+
+ list_items.push(line.get(1..).unwrap_or("").trim_start().to_string());
+
+ if let Some(next_line) = lines.next() {
+ line = next_line;
+ } else {
+ break;
+ }
+ }
+ ">" => {
+ // If the Gemtext line starts with an ">", it is a blockquote, so
+ // let's just clip off the line identifier.
+ nodes.push(Node::Blockquote(
+ line.get(1..).unwrap_or("").trim_start().to_string(),
+ ));
+
+ break;
+ }
+ "`" => {
+ // If the Gemtext line starts with a backtick, it is a list item, so
+ // let's enter a preformatted text context.
+ *in_preformatted = !*in_preformatted;
+
+ if *in_preformatted {
+ alt_text = line.get(3..).unwrap_or("").to_string();
+ line = lines.next().unwrap();
+ } else {
+ nodes.push(Node::PreformattedText {
+ alt_text: if alt_text.is_empty() {
+ None
+ } else {
+ Some(alt_text)
+ },
+ text: preformatted,
+ });
+
+ break;
+ }
+ }
+ "" if !*in_preformatted => {
+ // If the line has nothing on it, it is a whitespace line, as long as
+ // we aren't in a preformatted line context.
+ nodes.push(Node::Whitespace);
+
+ break;
+ }
+ // This as a catchall, it does a number of things.
+ _ => {
+ if *in_preformatted {
+ // If we are in a preformatted line context, add the line to the
+ // preformatted blocks content and increment the line.
+ preformatted.push_str(&format!("{}\n", line));
+
+ line = lines.next().unwrap();
+ } else {
+ // If we are in a list item and hit a catchall, that must mean that
+ // we encountered a line which is not a list line, so
+ // let's stop adding items to the list context.
+ if *in_list {
+ *in_list = false;
+
+ nodes.push(Node::Text(line.to_string()));
+
+ break;
+ }
+
+ nodes.push(Node::Text(line.to_string()));
+
+ break;
+ }
+ }
+ }
+ }
+
+ if !list_items.is_empty() {
+ nodes.reverse();
+ nodes.push(Node::List(list_items));
+ nodes.reverse();
+ }
+
+ nodes
+ }
+}
diff --git a/crates/germ/src/ast/macros.rs b/crates/germ/src/ast/macros.rs
new file mode 100644
index 0000000..b072daa
--- /dev/null
+++ b/crates/germ/src/ast/macros.rs
@@ -0,0 +1,52 @@
+// This file is part of Germ <https://github.com/gemrest/germ>.
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+//
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-only
+
+//! Macros to aid with various Germ-related functionalities.
+
+/// Convert a Gemini token tree into an `Ast`
+///
+/// # Example
+///
+/// ```rust
+/// // Using a value
+/// assert_eq!(
+/// germ::gemini_to_ast!("=> / A link!").to_gemtext(),
+/// // `to_gemtext` appends a newline to all responses, so let's make sure we
+/// // account for that.
+/// format!("{}\n", "=> / A link!"),
+/// );
+///
+/// /// Using raw Gemtext
+/// assert_eq!(
+/// germ::gemini_to_ast! {
+/// => / A link!
+/// => / Another link!
+/// }
+/// .to_gemtext(),
+/// format!("{}\n", "=> / A link!\n=> / Another link!"),
+/// );
+/// ```
+#[macro_export]
+macro_rules! gemini_to_ast {
+ ($gemini:expr) => {
+ germ::ast::Ast::from_string($gemini)
+ };
+ ($($gemini:tt)*) => {
+ germ::ast::Ast::from_string(germ_macros_impl::gemini_to_tt!($($gemini)*));
+ };
+}
diff --git a/crates/germ/src/ast/node.rs b/crates/germ/src/ast/node.rs
new file mode 100644
index 0000000..04296ff
--- /dev/null
+++ b/crates/germ/src/ast/node.rs
@@ -0,0 +1,173 @@
+// This file is part of Germ <https://github.com/gemrest/germ>.
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+//
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-only
+
+/// A Gemtext AST node.
+///
+/// Each Gemtext line is a `Node`, and some lines can even be grouped together,
+/// such as the `Node::List` `Node`!
+///
+/// # Gemtext Resources
+///
+/// - [Gemtext Documentation](https://gemini.circumlunar.space/docs/gemtext.gmi)
+/// - [Gemtext Cheatsheet](https://gemini.circumlunar.space/docs/cheatsheet.gmi).
+/// - [Gemini Specification](https://gemini.circumlunar.space/docs/specification.gmi).
+#[derive(Debug, PartialEq)]
+pub enum Node {
+ /// A text line
+ ///
+ /// # Example
+ ///
+ /// ```gemini
+ /// This is a text line
+ /// ```
+ Text(String),
+ /// A link line
+ ///
+ /// # Examples
+ ///
+ /// ```gemini
+ /// => /this-is-the-to This is the text
+ ///
+ /// => gemini://to.somewhere.link
+ /// ```
+ Link {
+ /// The location that a link line is pointing to
+ ///
+ /// # Examples
+ ///
+ /// ```gemini
+ /// => /this-is-the-to This is the text
+ ///
+ /// => gemini://to.somewhere.link
+ /// ```
+ to: String,
+ /// The text a link line *may* have
+ ///
+ /// # Examples
+ ///
+ /// ```gemini
+ /// => /this-is-the-to This line has text, unlike the next one.
+ ///
+ /// => gemini://to.somewhere.link
+ /// ```
+ text: Option<String>,
+ },
+ /// A heading line
+ ///
+ /// # Examples
+ ///
+ /// ```gemini
+ /// # This is a heading
+ ///
+ /// ## This is a sub-heading
+ ///
+ /// ### This is a sub-sub-heading
+ /// ```
+ Heading {
+ /// The level of a heading
+ ///
+ /// # Examples
+ ///
+ /// ```gemini
+ /// # This is a level 1 heading
+ ///
+ /// ## This is a level 2 sub-heading
+ ///
+ /// ### This is a level 3 sub-sub-heading
+ /// ```
+ level: usize,
+ /// The text of a heading
+ ///
+ /// # Examples
+ ///
+ /// ```gemini
+ /// # This is the headings text
+ ///
+ /// # This is also the headings text
+ /// ```
+ text: String,
+ },
+ /// A collection of sequential list item lines
+ ///
+ /// # Examples
+ ///
+ /// ```gemini
+ /// * These are
+ /// * sequential list
+ /// * items.
+ /// ```
+ List(Vec<String>),
+ /// A blockquote line
+ ///
+ /// # Examples
+ ///
+ /// ```gemini
+ /// > This is a blockquote line
+ ///
+ /// > This is also a blockquote line
+ /// ```
+ Blockquote(String),
+ /// A preformatted block
+ ///
+ /// # Examples
+ ///
+ /// Try to ignore the leading backslash in-front of the triple backticks,
+ /// they are there to not confuse the Markdown engine.
+ ///
+ /// ```gemini
+ /// \```This is the alt-text
+ /// This is the preformatted block
+ ///
+ /// This is the rest of the preformatted block
+ /// \```
+ /// ```
+ PreformattedText {
+ /// A preformatted blocks alt-text
+ ///
+ /// # Examples
+ ///
+ /// Try to ignore the leading backslash in-front of the triple backticks,
+ /// they are there to not confuse the Markdown engine.
+ ///
+ /// ```gemini
+ /// \```This is the alt-text
+ /// This is the preformatted block
+ ///
+ /// This is the rest of the preformatted block
+ /// \```
+ /// ```
+ alt_text: Option<String>,
+ /// A preformatted blocks content
+ ///
+ /// # Examples
+ ///
+ /// Try to ignore the leading backslash in-front of the triple backticks,
+ /// they are there to not confuse the Markdown engine.
+ ///
+ /// ```gemini
+ /// \```This is the alt-text
+ /// This is the preformatted blocks content
+ ///
+ /// This is the rest of the preformatted blocks content
+ /// \```
+ /// ```
+ text: String,
+ },
+ /// A whitespace line, a line which contains nothing but whitespace.
+ Whitespace,
+}
diff --git a/crates/germ/src/convert.rs b/crates/germ/src/convert.rs
new file mode 100644
index 0000000..c68696e
--- /dev/null
+++ b/crates/germ/src/convert.rs
@@ -0,0 +1,69 @@
+// This file is part of Germ <https://github.com/gemrest/germ>.
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+//
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-only
+
+//! Convert Gemtext into many types of markup.
+
+use crate::ast::Ast;
+
+mod html;
+mod markdown;
+
+/// Different targets to convert Gemtext to
+pub enum Target {
+ /// Convert Gemtext to HTML
+ HTML,
+ /// Convert Gemtext to Markdown
+ Markdown,
+}
+
+/// Convert AST'd Gemtext into an alternative markup format.
+///
+/// # Example
+///
+/// ```rust
+/// use germ::convert;
+///
+/// let _ = convert::from_ast(
+/// &germ::ast::Ast::from_string(r#"=> gemini://gem.rest/ GemRest"#),
+/// &convert::Target::HTML,
+/// );
+/// ```
+#[must_use]
+pub fn from_ast(source: &Ast, target: &Target) -> String {
+ match target {
+ Target::Markdown => markdown::convert(source.inner()),
+ Target::HTML => html::convert(source.inner()),
+ }
+}
+
+/// Convert raw Gemtext into an alternative markup format.
+///
+/// # Example
+///
+/// ```rust
+/// use germ::convert;
+///
+/// let _ = convert::from_string(
+/// r#"=> gemini://gem.rest/ GemRest"#,
+/// &convert::Target::HTML,
+/// );
+/// ```
+#[must_use]
+pub fn from_string(source: &str, target: &Target) -> String {
+ from_ast(&Ast::from_string(source), target)
+}
diff --git a/crates/germ/src/convert/html.rs b/crates/germ/src/convert/html.rs
new file mode 100644
index 0000000..7b1cafe
--- /dev/null
+++ b/crates/germ/src/convert/html.rs
@@ -0,0 +1,75 @@
+// This file is part of Germ <https://github.com/gemrest/germ>.
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+//
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-only
+
+use crate::ast::Node;
+
+pub fn convert(source: &[Node]) -> String {
+ let mut html = String::new();
+
+ // Since we have an AST tree of the Gemtext, it is very easy to convert from
+ // this AST tree to an alternative markup format.
+ for node in source {
+ match node {
+ Node::Text(text) => html.push_str(&format!("<p>{}</p>", text)),
+ Node::Link {
+ to,
+ text,
+ } => {
+ html.push_str(&format!(
+ "<a href=\"{}\">{}</a><br>",
+ to,
+ text.clone().unwrap_or_else(|| to.clone())
+ ));
+ }
+ Node::Heading {
+ level,
+ text,
+ } => {
+ html.push_str(&format!(
+ "<{}>{}</{0}>",
+ match level {
+ 1 => "h1",
+ 2 => "h2",
+ 3 => "h3",
+ _ => "p",
+ },
+ text
+ ));
+ }
+ Node::List(items) =>
+ html.push_str(&format!(
+ "<ul>{}</ul>",
+ items
+ .iter()
+ .map(|i| format!("<li>{}</li>", i))
+ .collect::<Vec<String>>()
+ .join("\n")
+ )),
+ Node::Blockquote(text) =>
+ html.push_str(&format!("<blockquote>{}</blockquote>", text)),
+ Node::PreformattedText {
+ text, ..
+ } => {
+ html.push_str(&format!("<pre>{}</pre>", text));
+ }
+ Node::Whitespace => {}
+ }
+ }
+
+ html
+}
diff --git a/crates/germ/src/convert/markdown.rs b/crates/germ/src/convert/markdown.rs
new file mode 100644
index 0000000..a38da9f
--- /dev/null
+++ b/crates/germ/src/convert/markdown.rs
@@ -0,0 +1,77 @@
+// This file is part of Germ <https://github.com/gemrest/germ>.
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+//
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-only
+
+use crate::ast::Node;
+
+pub fn convert(source: &[Node]) -> String {
+ let mut markdown = String::new();
+
+ // Since we have an AST tree of the Gemtext, it is very easy to convert from
+ // this AST tree to an alternative markup format.
+ for node in source {
+ match node {
+ Node::Text(text) => markdown.push_str(&format!("{}\n", text)),
+ Node::Link {
+ to,
+ text,
+ } =>
+ markdown.push_str(&*text.clone().map_or_else(
+ || format!("<{}>\n", to),
+ |text| format!("[{}]({})\n", text, to),
+ )),
+ Node::Heading {
+ level,
+ text,
+ } => {
+ markdown.push_str(&format!(
+ "{} {}\n",
+ match level {
+ 1 => "#",
+ 2 => "##",
+ 3 => "###",
+ _ => "",
+ },
+ text
+ ));
+ }
+ Node::List(items) =>
+ markdown.push_str(&format!(
+ "{}\n",
+ items
+ .iter()
+ .map(|i| format!("- {}", i))
+ .collect::<Vec<String>>()
+ .join("\n"),
+ )),
+ Node::Blockquote(text) => markdown.push_str(&format!("> {}\n", text)),
+ Node::PreformattedText {
+ alt_text,
+ text,
+ } => {
+ markdown.push_str(&format!(
+ "```{}\n{}```\n",
+ alt_text.clone().unwrap_or_else(|| "".to_string()),
+ text
+ ));
+ }
+ Node::Whitespace => markdown.push('\n'),
+ }
+ }
+
+ markdown
+}
diff --git a/crates/germ/src/lib.rs b/crates/germ/src/lib.rs
new file mode 100644
index 0000000..1289751
--- /dev/null
+++ b/crates/germ/src/lib.rs
@@ -0,0 +1,43 @@
+// This file is part of Germ <https://github.com/gemrest/germ>.
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+//
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-only
+
+#![deny(
+ warnings,
+ nonstandard_style,
+ unused,
+ future_incompatible,
+ rust_2018_idioms,
+ unsafe_code,
+ clippy::all,
+ clippy::nursery,
+ clippy::pedantic
+)]
+#![doc = include_str!("../../../README.md")]
+#![recursion_limit = "128"]
+
+#[cfg(feature = "ast")]
+pub mod ast;
+
+#[cfg(feature = "convert")]
+pub mod convert;
+
+#[cfg(feature = "request")]
+pub mod request;
+
+#[cfg(feature = "meta")]
+pub mod meta;
diff --git a/crates/germ/src/meta.rs b/crates/germ/src/meta.rs
new file mode 100644
index 0000000..f8a9dfb
--- /dev/null
+++ b/crates/germ/src/meta.rs
@@ -0,0 +1,162 @@
+// This file is part of Germ <https://github.com/gemrest/germ>.
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+//
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-only
+
+use std::collections::HashMap;
+
+/// Structure-ize a Gemini response's meta section into it's mime type and it's
+/// parameters.
+#[derive(Debug, Default)]
+pub struct Meta {
+ /// The mime type of a Gemini response
+ mime: String,
+ /// The parameters of a Gemini response
+ parameters: HashMap<String, String>,
+}
+impl ToString for Meta {
+ /// Convert a `Meta` into a `String`
+ ///
+ /// # Example
+ /// ```rust
+ /// let original_string = "text/gemini; hi=2; hi2=string=2";
+ ///
+ /// assert_eq!(
+ /// germ::meta::Meta::from_string(original_string).to_string(),
+ /// original_string
+ /// );
+ /// ```
+ fn to_string(&self) -> String {
+ format!("{}{}", self.mime, {
+ let mut parameters = self
+ .parameters
+ .iter()
+ .map(|(k, v)| format!("{}={}", *k, v))
+ .collect::<Vec<_>>();
+
+ parameters.sort();
+ parameters.reverse();
+
+ if parameters.is_empty() {
+ "".to_string()
+ } else {
+ format!("; {}", parameters.join("; "))
+ }
+ })
+ }
+}
+impl Meta {
+ /// Create a new `Meta`
+ ///
+ /// # Example
+ ///
+ /// ```rust
+ /// let mut meta = germ::meta::Meta::new();
+ /// ```
+ #[must_use]
+ pub fn new() -> Self { Self::default() }
+
+ /// Create a `Meta` from a string
+ ///
+ /// # Example
+ ///
+ /// ```rust
+ /// assert_eq!(
+ /// germ::meta::Meta::from_string("text/gemini; hi=2; hi2=string=2").mime(),
+ /// "text/gemini",
+ /// );
+ /// ```
+ #[must_use]
+ pub fn from_string(meta: &str) -> Self {
+ let mut metas = meta.split(';');
+ let mime = metas.next().unwrap_or("").to_string();
+ let mut parameters = HashMap::new();
+
+ for parameter in metas {
+ let key_value = parameter
+ .trim_start()
+ .split_at(parameter.find('=').unwrap_or(0));
+
+ parameters.insert(
+ key_value.0.to_string().replace('=', ""),
+ key_value.1.to_string(),
+ );
+ }
+
+ Self {
+ mime,
+ parameters,
+ }
+ }
+
+ /// Obtain non-mutable access to the mime of the `Meta`
+ ///
+ /// # Example
+ ///
+ /// ```rust
+ /// assert_eq!(
+ /// germ::meta::Meta::from_string("text/gemini; hi=2; hi2=string=2").mime(),
+ /// "text/gemini",
+ /// );
+ /// ```
+ #[must_use]
+ pub fn mime(&self) -> &str { &self.mime }
+
+ /// Obtain mutable access to the mime of the `Meta`
+ ///
+ /// # Example
+ ///
+ /// ```rust
+ /// let mut meta = germ::meta::Meta::new();
+ ///
+ /// *meta.mime_mut() = "text/gemini".to_string();
+ /// ```
+ pub fn mime_mut(&mut self) -> &mut String { &mut self.mime }
+
+ /// Obtain non-mutable access to the parameters of the `Meta`
+ ///
+ /// # Example
+ ///
+ /// ```rust
+ /// assert_eq!(
+ /// germ::meta::Meta::from_string("text/gemini; hi=2; hi2=string=2")
+ /// .parameters()
+ /// .get("hi2"),
+ /// Some(&"string=2".to_string()),
+ /// );
+ /// ```
+ #[must_use]
+ pub const fn parameters(&self) -> &HashMap<String, String> {
+ &self.parameters
+ }
+
+ /// Obtain mutable access to the parameters of the `Meta`
+ ///
+ /// # Example
+ ///
+ /// ```rust
+ /// let mut meta = germ::meta::Meta::new();
+ /// let mut parameters = std::collections::HashMap::new();
+ ///
+ /// parameters.insert("hi".to_string(), "2".to_string());
+ /// parameters.insert("hi2".to_string(), "string=2".to_string());
+ ///
+ /// *meta.parameters_mut() = parameters;
+ /// ```
+ pub fn parameters_mut(&mut self) -> &mut HashMap<String, String> {
+ &mut self.parameters
+ }
+}
diff --git a/crates/germ/src/request.rs b/crates/germ/src/request.rs
new file mode 100644
index 0000000..07d3552
--- /dev/null
+++ b/crates/germ/src/request.rs
@@ -0,0 +1,73 @@
+// This file is part of Germ <https://github.com/gemrest/germ>.
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+//
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-only
+
+//! Make Gemini requests and get sane, structured results
+
+mod response;
+mod status;
+mod verifier;
+
+use std::io::{Read, Write};
+
+pub use response::Response;
+pub use status::Status;
+use verifier::GermVerifier;
+
+/// Make a request to a Gemini server. The `url` **should** be prefixed with a
+/// scheme (e.g. "gemini://").
+///
+/// # Example
+///
+/// ```rust
+/// match germ::request::request(&url::Url::parse("gemini://fuwn.me").unwrap()) {
+/// Ok(response) => println!("{:?}", response),
+/// Err(_) => {}
+/// }
+/// ```
+///
+/// # Errors
+/// - May error if the URL is invalid
+/// - May error if the TLS write fails
+/// - May error if the TLS read fails
+pub fn request(url: &url::Url) -> anyhow::Result<Response> {
+ let config = rustls::ClientConfig::builder()
+ .with_safe_defaults()
+ .with_custom_certificate_verifier(std::sync::Arc::new(GermVerifier::new()))
+ .with_no_client_auth();
+ let mut connection = rustls::ClientConnection::new(
+ std::sync::Arc::new(config),
+ url.domain().unwrap_or("").try_into()?,
+ )?;
+ let mut stream = std::net::TcpStream::connect(format!(
+ "{}:{}",
+ url.domain().unwrap_or(""),
+ url.port().unwrap_or(1965)
+ ))?;
+ let mut tls = rustls::Stream::new(&mut connection, &mut stream);
+
+ tls.write_all(format!("{}\r\n", url).as_bytes())?;
+
+ let mut plain_text = Vec::new();
+
+ tls.read_to_end(&mut plain_text)?;
+
+ Ok(Response::new(
+ &plain_text,
+ tls.conn.negotiated_cipher_suite(),
+ ))
+}
diff --git a/crates/germ/src/request/response.rs b/crates/germ/src/request/response.rs
new file mode 100644
index 0000000..5e1f436
--- /dev/null
+++ b/crates/germ/src/request/response.rs
@@ -0,0 +1,71 @@
+// This file is part of Germ <https://github.com/gemrest/germ>.
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+//
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-only
+
+use rustls::SupportedCipherSuite;
+
+use crate::request::Status;
+
+#[derive(Debug)]
+pub struct Response {
+ status: Status,
+ meta: String,
+ content: Option<String>,
+ size: usize,
+ suite: Option<SupportedCipherSuite>,
+}
+impl Response {
+ pub(super) fn new(data: &[u8], suite: Option<SupportedCipherSuite>) -> Self {
+ let string_form = String::from_utf8_lossy(data).to_string();
+ let mut content = None;
+ let header;
+
+ if string_form.ends_with("\r\n") {
+ header = string_form;
+ } else {
+ let mut string_split = string_form.split("\r\n");
+
+ header = string_split.next().unwrap_or("").to_string();
+ content = Some(string_split.collect());
+ }
+
+ let header_split = header.split_at(2);
+
+ Self {
+ status: Status::from(header_split.0.parse::<i32>().unwrap_or(0)),
+ meta: header_split.1.trim_start().to_string(),
+ content,
+ size: data.len(),
+ suite,
+ }
+ }
+
+ #[must_use]
+ pub const fn status(&self) -> &Status { &self.status }
+
+ #[must_use]
+ pub fn meta(&self) -> &str { &self.meta }
+
+ #[must_use]
+ pub const fn content(&self) -> &Option<String> { &self.content }
+
+ #[must_use]
+ pub const fn size(&self) -> &usize { &self.size }
+
+ #[must_use]
+ pub const fn suite(&self) -> &Option<SupportedCipherSuite> { &self.suite }
+}
diff --git a/crates/germ/src/request/status.rs b/crates/germ/src/request/status.rs
new file mode 100644
index 0000000..f46059a
--- /dev/null
+++ b/crates/germ/src/request/status.rs
@@ -0,0 +1,110 @@
+// This file is part of Germ <https://github.com/gemrest/germ>.
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+//
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-only
+
+use std::{fmt, fmt::Formatter};
+
+/// Simple Gemini status reporting
+///
+/// # Examples
+///
+/// ```rust
+/// use germ::request::Status;
+///
+/// assert_eq!(Status::from(10), Status::Input);
+/// assert_eq!(i32::from(Status::Input), 10);
+/// ```
+#[derive(Debug, PartialEq)]
+pub enum Status {
+ Input,
+ SensitiveInput,
+ Success,
+ TemporaryRedirect,
+ PermanentRedirect,
+ TemporaryFailure,
+ ServerUnavailable,
+ CGIError,
+ ProxyError,
+ SlowDown,
+ PermanentFailure,
+ NotFound,
+ Gone,
+ ProxyRefused,
+ BadRequest,
+ ClientCertificateRequired,
+ CertificateNotAuthorised,
+ CertificateNotValid,
+ Unsupported,
+}
+impl Default for Status {
+ fn default() -> Self { Self::Success }
+}
+impl From<Status> for i32 {
+ fn from(n: Status) -> Self {
+ match n {
+ Status::Input => 10,
+ Status::SensitiveInput => 11,
+ Status::Success => 20,
+ Status::TemporaryRedirect => 30,
+ Status::PermanentRedirect => 31,
+ Status::TemporaryFailure => 40,
+ Status::ServerUnavailable => 41,
+ Status::CGIError => 42,
+ Status::ProxyError => 43,
+ Status::SlowDown => 44,
+ Status::PermanentFailure => 50,
+ Status::NotFound => 51,
+ Status::Gone => 52,
+ Status::ProxyRefused => 53,
+ Status::BadRequest => 59,
+ Status::ClientCertificateRequired => 60,
+ Status::CertificateNotAuthorised => 61,
+ Status::CertificateNotValid => 62,
+ Status::Unsupported => 0,
+ }
+ }
+}
+impl From<i32> for Status {
+ fn from(n: i32) -> Self {
+ match n {
+ 10 => Self::Input,
+ 11 => Self::SensitiveInput,
+ 20 => Self::Success,
+ 30 => Self::TemporaryRedirect,
+ 31 => Self::PermanentRedirect,
+ 40 => Self::TemporaryFailure,
+ 41 => Self::ServerUnavailable,
+ 42 => Self::CGIError,
+ 43 => Self::ProxyError,
+ 44 => Self::SlowDown,
+ 50 => Self::PermanentFailure,
+ 51 => Self::NotFound,
+ 52 => Self::Gone,
+ 53 => Self::ProxyRefused,
+ 59 => Self::BadRequest,
+ 60 => Self::ClientCertificateRequired,
+ 61 => Self::CertificateNotAuthorised,
+ 62 => Self::CertificateNotValid,
+ _ => Self::Unsupported,
+ }
+ }
+}
+impl fmt::Display for Status {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ write!(f, "{:?}", self)
+ }
+}
diff --git a/crates/germ/src/request/verifier.rs b/crates/germ/src/request/verifier.rs
new file mode 100644
index 0000000..d6511c3
--- /dev/null
+++ b/crates/germ/src/request/verifier.rs
@@ -0,0 +1,39 @@
+// This file is part of Germ <https://github.com/gemrest/germ>.
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+//
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-only
+
+use std::time::SystemTime;
+
+use rustls::{client, client::ServerCertVerified, Certificate};
+
+pub(super) struct GermVerifier;
+impl GermVerifier {
+ pub const fn new() -> Self { Self {} }
+}
+impl client::ServerCertVerifier for GermVerifier {
+ fn verify_server_cert(
+ &self,
+ _end_entity: &Certificate,
+ _intermediates: &[Certificate],
+ _server_name: &client::ServerName,
+ _scts: &mut dyn Iterator<Item = &[u8]>,
+ _ocsp_response: &[u8],
+ _now: SystemTime,
+ ) -> Result<ServerCertVerified, rustls::Error> {
+ Ok(ServerCertVerified::assertion())
+ }
+}
diff --git a/crates/germ/tests/ast.rs b/crates/germ/tests/ast.rs
new file mode 100644
index 0000000..302b9c9
--- /dev/null
+++ b/crates/germ/tests/ast.rs
@@ -0,0 +1,131 @@
+// This file is part of Germ <https://github.com/gemrest/germ>.
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+//
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-only
+
+#[cfg(test)]
+mod test {
+ use germ::ast::{Ast, Node};
+
+ const EXAMPLE_GEMTEXT: &str = r#"```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
+
+That was a link without text."#;
+
+ #[test]
+ fn build_multi_line_list_with_text() {
+ assert_eq!(
+ *Ast::from_string("* item1\n* 2\nhi text").inner(),
+ vec![
+ Node::List(vec!["item1".to_string(), "2".to_string()]),
+ Node::Text("hi text".to_string()),
+ ],
+ );
+ }
+
+ #[test]
+ fn build_multi_line_vec() {
+ assert_eq!(
+ *Ast::from_string("=> /test hi\nhi there\n> hi").inner(),
+ vec![
+ Node::Link {
+ to: "/test".to_string(),
+ text: Some("hi".to_string()),
+ },
+ Node::Text("hi there".to_string()),
+ Node::Blockquote("hi".to_string()),
+ ],
+ );
+ }
+
+ #[test]
+ fn build_single_0th_from_vec() {
+ assert_eq!(
+ Ast::from_string("=> /test hi").inner(),
+ &vec![Node::Link {
+ to: "/test".to_string(),
+ text: Some("hi".to_string()),
+ }],
+ );
+ }
+
+ #[test]
+ fn build_single_element() {
+ assert_eq!(
+ Ast::from_string("=> /test hi").inner().get(0).unwrap(),
+ &Node::Link {
+ to: "/test".to_string(),
+ text: Some("hi".to_string()),
+ },
+ );
+ }
+
+ #[test]
+ fn gemtext_to_ast_then_ast_to_gemtext() {
+ assert_eq!(
+ Ast::from_string(EXAMPLE_GEMTEXT).to_gemtext(),
+ // `to_gemtext` appends a newline to all responses, so let's make sure we
+ // account for that.
+ format!("{}\n", EXAMPLE_GEMTEXT),
+ );
+ }
+
+ #[test]
+ fn gemtext_to_ast_then_ast_to_gemtext_macro() {
+ assert_eq!(
+ germ::gemini_to_ast!(EXAMPLE_GEMTEXT).to_gemtext(),
+ // `to_gemtext` appends a newline to all responses, so let's make sure we
+ // account for that.
+ format!("{}\n", EXAMPLE_GEMTEXT),
+ );
+ }
+
+ #[test]
+ fn gemtext_to_ast_then_ast_to_gemtext_macro_simple() {
+ assert_eq!(
+ germ::gemini_to_ast! {
+ => / A link!
+ => / Another link!
+ }
+ .to_gemtext(),
+ format!("{}\n", "=> / A link!\n=> / Another link!"),
+ );
+ }
+}
diff --git a/crates/germ/tests/convert.rs b/crates/germ/tests/convert.rs
new file mode 100644
index 0000000..40f337d
--- /dev/null
+++ b/crates/germ/tests/convert.rs
@@ -0,0 +1,61 @@
+// This file is part of Germ <https://github.com/gemrest/germ>.
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+//
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-only
+
+#[cfg(test)]
+mod test {
+ use germ::convert::{from_string, Target};
+
+ #[test]
+ fn convert_from_string_to_html_single_line() {
+ assert_eq!(from_string("hi", &Target::HTML), "<p>hi</p>",);
+ }
+
+ #[test]
+ fn convert_from_string_to_html_multi_line() {
+ assert_eq!(
+ from_string("hi\n# hi", &Target::HTML),
+ "<p>hi</p><h1>hi</h1>",
+ );
+ }
+
+ #[test]
+ fn convert_from_string_to_html_single_link() {
+ assert_eq!(
+ from_string("=> /to hello !", &Target::HTML),
+ "<a href=\"/to\">hello !</a><br>",
+ );
+ }
+
+ #[test]
+ fn convert_from_string_to_markdown_single_line() {
+ assert_eq!(from_string("hi", &Target::Markdown), "hi\n",);
+ }
+
+ #[test]
+ fn convert_from_string_to_markdown_multi_line() {
+ assert_eq!(from_string("hi\n# hi", &Target::Markdown), "hi\n# hi\n",);
+ }
+
+ #[test]
+ fn convert_from_string_to_markdown_single_link() {
+ assert_eq!(
+ from_string("=> /to hello !", &Target::Markdown),
+ "[hello !](/to)\n",
+ );
+ }
+}
diff --git a/crates/germ/tests/meta.rs b/crates/germ/tests/meta.rs
new file mode 100644
index 0000000..70c8adc
--- /dev/null
+++ b/crates/germ/tests/meta.rs
@@ -0,0 +1,101 @@
+// This file is part of Germ <https://github.com/gemrest/germ>.
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+//
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-only
+
+#[cfg(test)]
+mod test {
+ use germ::meta::Meta;
+
+ #[test]
+ fn construct_meta_with_mime() {
+ let mut meta = Meta::new();
+
+ *meta.mime_mut() = "text/gemini".to_string();
+
+ assert_eq!(meta.to_string(), "text/gemini");
+ }
+
+ #[test]
+ fn construct_meta_with_mime_and_parameters() {
+ let mut meta = Meta::new();
+ let mut parameters = std::collections::HashMap::new();
+
+ parameters.insert("hi".to_string(), "2".to_string());
+ parameters.insert("hi2".to_string(), "string=2".to_string());
+
+ *meta.mime_mut() = "text/gemini".to_string();
+ *meta.parameters_mut() = parameters;
+
+ assert_eq!(meta.to_string(), "text/gemini; hi=2; hi2=string=2");
+ }
+
+ #[test]
+ fn meta_to_string_without_parameters() {
+ let original_string = "text/gemini";
+
+ assert_eq!(
+ Meta::from_string(original_string).to_string(),
+ original_string
+ );
+ }
+
+ #[test]
+ fn meta_to_string_with_parameters() {
+ let original_string = "text/gemini; hi=2; hi2=string=2";
+
+ assert_eq!(
+ Meta::from_string(original_string).to_string(),
+ original_string
+ );
+ }
+
+ #[test]
+ fn meta_to_mime_without_parameters() {
+ let meta = Meta::from_string("text/gemini");
+
+ assert_eq!(meta.mime(), "text/gemini");
+ assert_eq!(meta.parameters().len(), 0);
+ }
+
+ #[test]
+ fn meta_to_map_mime() {
+ assert_eq!(
+ Meta::from_string("text/gemini; hi=2; hi2=string=2").mime(),
+ "text/gemini",
+ );
+ }
+
+ #[test]
+ fn meta_to_map_with_parameters() {
+ assert_eq!(
+ Meta::from_string("text/gemini; hi=2; hi2=string=2")
+ .parameters()
+ .get("hi2"),
+ Some(&"string=2".to_string()),
+ );
+ }
+
+ #[test]
+ fn meta_to_map_length() {
+ assert_eq!(
+ Meta::from_string("text/gemini; hi=2; hi2=string=2")
+ .parameters()
+ .len(),
+ 2,
+ );
+ }
+}
diff --git a/crates/germ/tests/status.rs b/crates/germ/tests/status.rs
new file mode 100644
index 0000000..51f3f66
--- /dev/null
+++ b/crates/germ/tests/status.rs
@@ -0,0 +1,32 @@
+// This file is part of Germ <https://github.com/gemrest/germ>.
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+//
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-only
+
+#[cfg(test)]
+mod test {
+ use germ::request::Status;
+
+ #[test]
+ fn status_from_i32() {
+ assert_eq!(Status::from(10), Status::Input);
+ }
+
+ #[test]
+ fn i32_from_status() {
+ assert_eq!(i32::from(Status::Input), 10);
+ }
+}