Compare commits
13 commits
Author | SHA1 | Date | |
---|---|---|---|
Emi Tatsuo | 4c63370a26 | ||
Emi Tatsuo | fb4a33685b | ||
Emi Tatsuo | ffc86af284 | ||
Emi Tatsuo | 367cc17e8f | ||
Emi Tatsuo | 1c1e2567f5 | ||
Emi Tatsuo | 485f579e4c | ||
Emi Tatsuo | 9ec1a5663d | ||
Emii Tatsuo | e95cbd70e9 | ||
Emii Tatsuo | 4ba099f947 | ||
Emii Tatsuo | 9aa90c3e59 | ||
Emii Tatsuo | a1d52faa9d | ||
0af7243517 | |||
00aa1f96f4 |
|
@ -15,8 +15,8 @@ include = ["src/**", "Cargo.*", "CHANGELOG.md", "LICENSE*", "README.md"]
|
||||||
default = ["certgen"]
|
default = ["certgen"]
|
||||||
user_management = ["sled", "bincode", "serde/derive", "crc32fast", "lazy_static"]
|
user_management = ["sled", "bincode", "serde/derive", "crc32fast", "lazy_static"]
|
||||||
user_management_advanced = ["rust-argon2", "user_management"]
|
user_management_advanced = ["rust-argon2", "user_management"]
|
||||||
user_management_routes = ["user_management"]
|
user_management_routes = ["user_management", "gemtext"]
|
||||||
serve_dir = ["mime_guess", "tokio/fs"]
|
serve_dir = ["mime_guess", "tokio/fs", "gemtext"]
|
||||||
ratelimiting = ["dashmap"]
|
ratelimiting = ["dashmap"]
|
||||||
certgen = ["rcgen", "gemini_srv"]
|
certgen = ["rcgen", "gemini_srv"]
|
||||||
gemini_srv = ["tokio-rustls", "webpki", "rustls", "ring"]
|
gemini_srv = ["tokio-rustls", "webpki", "rustls", "ring"]
|
||||||
|
@ -35,6 +35,7 @@ rustls = { version = "0.19", features = ["dangerous_configuration"], optional =
|
||||||
webpki = { version = "0.21.0", optional = true}
|
webpki = { version = "0.21.0", optional = true}
|
||||||
tokio-rustls = { version = "0.21.0", optional = true}
|
tokio-rustls = { version = "0.21.0", optional = true}
|
||||||
mime_guess = { version = "2.0.3", optional = true }
|
mime_guess = { version = "2.0.3", optional = true }
|
||||||
|
gemtext = { git = "https://tulpa.dev/alch_emii/maj-prs.git", branch = "local-main", optional = true }
|
||||||
dashmap = { version = "3.11.10", optional = true }
|
dashmap = { version = "3.11.10", optional = true }
|
||||||
sled = { version = "0.34.6", optional = true }
|
sled = { version = "0.34.6", optional = true }
|
||||||
bincode = { version = "1.3.1", optional = true }
|
bincode = { version = "1.3.1", optional = true }
|
||||||
|
@ -55,6 +56,10 @@ required-features = ["user_management_routes"]
|
||||||
name = "serve_dir"
|
name = "serve_dir"
|
||||||
required-features = ["serve_dir"]
|
required-features = ["serve_dir"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "document"
|
||||||
|
required-features = ["gemtext"]
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "ratelimiting"
|
name = "ratelimiting"
|
||||||
required-features = ["ratelimiting"]
|
required-features = ["ratelimiting"]
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
use anyhow::*;
|
use anyhow::*;
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
use kochab::{Server, Response, Document};
|
use kochab::{Server, Response, Gemtext};
|
||||||
use kochab::document::HeadingLevel::*;
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
|
@ -9,10 +8,11 @@ async fn main() -> Result<()> {
|
||||||
.filter_module("kochab", LevelFilter::Debug)
|
.filter_module("kochab", LevelFilter::Debug)
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
let response: Response = Document::new()
|
// Generate a fancy procedural response
|
||||||
.add_preformatted_with_alt("kochab", include_str!("kochab_logo.txt"))
|
let response: Response = Gemtext::new()
|
||||||
.add_blank_line()
|
.preformatted("kochab", include_str!("kochab_logo.txt"))
|
||||||
.add_text(
|
.blank_line()
|
||||||
|
.text(
|
||||||
concat!(
|
concat!(
|
||||||
"Kochab is an extension & a fork of the Gemini SDK [northstar]. Where",
|
"Kochab is an extension & a fork of the Gemini SDK [northstar]. Where",
|
||||||
" northstar creates an efficient and flexible foundation for Gemini projects,",
|
" northstar creates an efficient and flexible foundation for Gemini projects,",
|
||||||
|
@ -21,24 +21,26 @@ async fn main() -> Result<()> {
|
||||||
" worrying about needing to build the tools to get there."
|
" worrying about needing to build the tools to get there."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.add_blank_line()
|
.blank_line()
|
||||||
.add_link("https://github.com/Alch-Emi/kochab", "GitHub")
|
.link("https://github.com/Alch-Emi/kochab", Some("GitLab".to_string()))
|
||||||
.add_blank_line()
|
.blank_line()
|
||||||
.add_heading(H2, "Usage")
|
.heading(2, "Usage")
|
||||||
.add_blank_line()
|
.blank_line()
|
||||||
.add_text("Add the latest version of kochab to your `Cargo.toml`.")
|
.text("Add the latest version of kochab to your `Cargo.toml`.")
|
||||||
.add_blank_line()
|
.blank_line()
|
||||||
.add_preformatted_with_alt("toml", r#"kochab = { git = "https://github.com/Alch-Emi/kochab.git" }"#)
|
.preformatted("toml", r#"kochab = { git = "https://github.com/Alch-Emi/kochab.git" }"#)
|
||||||
.add_blank_line()
|
.blank_line()
|
||||||
.add_heading(H2, "Generating a key & certificate")
|
.heading(2, "Generating a key & certificate")
|
||||||
.add_blank_line()
|
.blank_line()
|
||||||
.add_preformatted_with_alt("sh", concat!(
|
.preformatted("sh", concat!(
|
||||||
"mkdir cert && cd cert\n",
|
"mkdir cert && cd cert\n",
|
||||||
"openssl req -x509 -nodes -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365",
|
"openssl req -x509 -nodes -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365",
|
||||||
))
|
))
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
Server::new()
|
Server::new()
|
||||||
|
// You can also return the response from any one of your response handlers, but if
|
||||||
|
// you want to serve a static response, this works too
|
||||||
.add_route("/", response)
|
.add_route("/", response)
|
||||||
.serve_unix("kochab.sock")
|
.serve_unix("kochab.sock")
|
||||||
.await
|
.await
|
||||||
|
|
|
@ -2,7 +2,7 @@ use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::*;
|
use anyhow::*;
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
use kochab::{Server, Request, Response, Document};
|
use kochab::{Server, Request, Response, Gemtext};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
|
@ -18,17 +18,20 @@ async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_request(request: Request) -> Result<Response> {
|
async fn handle_request(request: Request) -> Result<Response> {
|
||||||
let mut document = Document::new();
|
let mut document = if let Some("limit") = request.trailing_segments().get(0).map(String::as_str) {
|
||||||
|
Gemtext::new()
|
||||||
if let Some("limit") = request.trailing_segments().get(0).map(String::as_str) {
|
.text("You're on a rate limited page!")
|
||||||
document.add_text("You're on a rate limited page!")
|
.text("You can only access this page twice per minute")
|
||||||
.add_text("You can only access this page twice per minute");
|
|
||||||
} else {
|
} else {
|
||||||
document.add_text("You're on a normal page!")
|
Gemtext::new()
|
||||||
.add_text("You can access this page as much as you like.");
|
.text("You're on a normal page!")
|
||||||
}
|
.text("You can access this page as much as you like.")
|
||||||
document.add_blank_line()
|
};
|
||||||
.add_link("/limit", "Go to rate limited page")
|
|
||||||
.add_link("/", "Go to a page that's not rate limited");
|
document = document
|
||||||
|
.blank_line()
|
||||||
|
.link("/limit", Some("Go to rate limited page".to_string()))
|
||||||
|
.link("/", Some("Go to a page that's not rate limited".to_string()));
|
||||||
|
|
||||||
Ok(document.into())
|
Ok(document.into())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use anyhow::*;
|
use anyhow::*;
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
use kochab::{Document, document::HeadingLevel, Request, Response};
|
use kochab::{Gemtext, Request, Response};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
|
@ -17,32 +17,29 @@ async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_base(req: Request) -> Result<Response> {
|
async fn handle_base(req: Request) -> Result<Response> {
|
||||||
let doc = generate_doc("base", &req);
|
Ok(generate_resp("base", &req))
|
||||||
Ok(doc.into())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_short(req: Request) -> Result<Response> {
|
async fn handle_short(req: Request) -> Result<Response> {
|
||||||
let doc = generate_doc("short", &req);
|
Ok(generate_resp("short", &req))
|
||||||
Ok(doc.into())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_long(req: Request) -> Result<Response> {
|
async fn handle_long(req: Request) -> Result<Response> {
|
||||||
let doc = generate_doc("long", &req);
|
Ok(generate_resp("long", &req))
|
||||||
Ok(doc.into())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_doc(route_name: &str, req: &Request) -> Document {
|
fn generate_resp(route_name: &str, req: &Request) -> Response {
|
||||||
let trailing = req.trailing_segments().join("/");
|
let trailing = req.trailing_segments().join("/");
|
||||||
let mut doc = Document::new();
|
Gemtext::new()
|
||||||
doc.add_heading(HeadingLevel::H1, "Routing Demo")
|
.heading(1, "Routing Demo")
|
||||||
.add_text(&format!("You're currently on the {} route", route_name))
|
.text(&format!("You're currently on the {} route", route_name))
|
||||||
.add_text(&format!("Trailing segments: /{}", trailing))
|
.text(&format!("Trailing segments: /{}", trailing))
|
||||||
.add_blank_line()
|
.blank_line()
|
||||||
.add_text("Here's some links to try:")
|
.text("Here's some links to try:")
|
||||||
.add_link_without_label("/")
|
.link("/", Option::<String>::None)
|
||||||
.add_link_without_label("/route")
|
.link("/route", Option::<String>::None)
|
||||||
.add_link_without_label("/route/long")
|
.link("/route/long", Option::<String>::None)
|
||||||
.add_link_without_label("/route/not_real")
|
.link("/route/not_real", Option::<String>::None)
|
||||||
.add_link_without_label("/rowte");
|
.link("/rowte", Option::<String>::None)
|
||||||
doc
|
.into()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use anyhow::*;
|
use anyhow::*;
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
use kochab::{
|
use kochab::{
|
||||||
Document,
|
Gemtext,
|
||||||
Request,
|
Request,
|
||||||
Response,
|
Response,
|
||||||
Server,
|
Server,
|
||||||
|
@ -50,12 +50,12 @@ async fn main() -> Result<()> {
|
||||||
/// certificate will be prompted to add a certificate and register.
|
/// certificate will be prompted to add a certificate and register.
|
||||||
async fn handle_main(_req: Request, user: RegisteredUser<String>) -> Result<Response> {
|
async fn handle_main(_req: Request, user: RegisteredUser<String>) -> Result<Response> {
|
||||||
// If the user is signed in, render and return their page
|
// If the user is signed in, render and return their page
|
||||||
let response = Document::new()
|
let response = Gemtext::new()
|
||||||
.add_text("Your personal secret string:")
|
.text("Your personal secret string:")
|
||||||
.add_text(user.as_ref())
|
.text(user.as_ref())
|
||||||
.add_blank_line()
|
.blank_line()
|
||||||
.add_link("/update", "Change your string")
|
.link("/update", Some("Change your string".to_string()))
|
||||||
.add_link("/account", "Update your account")
|
.link("/account", Some("Update your account".to_string()))
|
||||||
.into();
|
.into();
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
@ -72,10 +72,10 @@ async fn handle_update(_request: Request, mut user: RegisteredUser<String>, inpu
|
||||||
*user.as_mut() = input;
|
*user.as_mut() = input;
|
||||||
|
|
||||||
// Render a response
|
// Render a response
|
||||||
let response = Document::new()
|
let response = Gemtext::new()
|
||||||
.add_text("String updated!")
|
.text("String updated!")
|
||||||
.add_blank_line()
|
.blank_line()
|
||||||
.add_link("/", "Back")
|
.link("/", Some("Back".to_string()))
|
||||||
.into();
|
.into();
|
||||||
Ok(response)
|
Ok(response)
|
||||||
|
|
||||||
|
|
|
@ -3,19 +3,13 @@
|
||||||
//! ⚠️ Docs still under construction & API not yet stable ⚠️
|
//! ⚠️ Docs still under construction & API not yet stable ⚠️
|
||||||
#![allow(missing_docs)]
|
#![allow(missing_docs)]
|
||||||
|
|
||||||
#[cfg(feature="serve_dir")]
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
#[cfg(feature="serve_dir")]
|
|
||||||
use tokio::{
|
use tokio::{
|
||||||
fs::{self, File},
|
fs::{self, File},
|
||||||
io,
|
io,
|
||||||
};
|
};
|
||||||
#[cfg(feature="serve_dir")]
|
|
||||||
use crate::types::{Document, document::HeadingLevel::*};
|
|
||||||
#[cfg(feature="serve_dir")]
|
|
||||||
use crate::types::Response;
|
use crate::types::Response;
|
||||||
|
|
||||||
#[cfg(feature="serve_dir")]
|
|
||||||
pub async fn serve_file<P: AsRef<Path>>(path: P, mime: &str) -> Response {
|
pub async fn serve_file<P: AsRef<Path>>(path: P, mime: &str) -> Response {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
|
|
||||||
|
@ -33,7 +27,6 @@ pub async fn serve_file<P: AsRef<Path>>(path: P, mime: &str) -> Response {
|
||||||
Response::success(mime, file)
|
Response::success(mime, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature="serve_dir")]
|
|
||||||
pub async fn serve_dir<D: AsRef<Path>, P: AsRef<Path>>(dir: D, virtual_path: &[P]) -> Response {
|
pub async fn serve_dir<D: AsRef<Path>, P: AsRef<Path>>(dir: D, virtual_path: &[P]) -> Response {
|
||||||
debug!("Dir: {}", dir.as_ref().display());
|
debug!("Dir: {}", dir.as_ref().display());
|
||||||
let dir = dir.as_ref();
|
let dir = dir.as_ref();
|
||||||
|
@ -87,7 +80,6 @@ pub async fn serve_dir<D: AsRef<Path>, P: AsRef<Path>>(dir: D, virtual_path: &[P
|
||||||
serve_dir_listing(path, virtual_path).await
|
serve_dir_listing(path, virtual_path).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature="serve_dir")]
|
|
||||||
async fn serve_dir_listing<P: AsRef<Path>, B: AsRef<Path>>(path: P, virtual_path: &[B]) -> Response {
|
async fn serve_dir_listing<P: AsRef<Path>, B: AsRef<Path>>(path: P, virtual_path: &[B]) -> Response {
|
||||||
let mut dir = match fs::read_dir(path.as_ref()).await {
|
let mut dir = match fs::read_dir(path.as_ref()).await {
|
||||||
Ok(dir) => dir,
|
Ok(dir) => dir,
|
||||||
|
@ -102,13 +94,13 @@ async fn serve_dir_listing<P: AsRef<Path>, B: AsRef<Path>>(path: P, virtual_path
|
||||||
};
|
};
|
||||||
|
|
||||||
let breadcrumbs: PathBuf = virtual_path.iter().collect();
|
let breadcrumbs: PathBuf = virtual_path.iter().collect();
|
||||||
let mut document = Document::new();
|
let mut document = gemtext::Builder::new();
|
||||||
|
|
||||||
document.add_heading(H1, format!("Index of /{}", breadcrumbs.display()));
|
document = document.heading(1, format!("Index of /{}", breadcrumbs.display()))
|
||||||
document.add_blank_line();
|
.blank_line();
|
||||||
|
|
||||||
if virtual_path.get(0).map(<_>::as_ref) != Some(Path::new("")) {
|
if virtual_path.get(0).map(<_>::as_ref) != Some(Path::new("")) {
|
||||||
document.add_link("..", "📁 ../");
|
document = document.link("..", Some("📁 ../".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
while let Some(entry) = dir.next_entry().await.expect("Failed to list directory") {
|
while let Some(entry) = dir.next_entry().await.expect("Failed to list directory") {
|
||||||
|
@ -118,17 +110,16 @@ async fn serve_dir_listing<P: AsRef<Path>, B: AsRef<Path>>(path: P, virtual_path
|
||||||
let trailing_slash = if is_dir { "/" } else { "" };
|
let trailing_slash = if is_dir { "/" } else { "" };
|
||||||
let uri = format!("./{}{}", file_name, trailing_slash);
|
let uri = format!("./{}{}", file_name, trailing_slash);
|
||||||
|
|
||||||
document.add_link(uri.as_str(), format!("{icon} {name}{trailing_slash}",
|
document = document.link(uri.as_str(), Some(format!("{icon} {name}{trailing_slash}",
|
||||||
icon = if is_dir { '📁' } else { '📄' },
|
icon = if is_dir { '📁' } else { '📄' },
|
||||||
name = file_name,
|
name = file_name,
|
||||||
trailing_slash = trailing_slash
|
trailing_slash = trailing_slash
|
||||||
));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
document.into()
|
document.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature="serve_dir")]
|
|
||||||
pub fn guess_mime_from_path<P: AsRef<Path>>(path: P) -> &'static str {
|
pub fn guess_mime_from_path<P: AsRef<Path>>(path: P) -> &'static str {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
let extension = path.extension().and_then(|s| s.to_str());
|
let extension = path.extension().and_then(|s| s.to_str());
|
||||||
|
@ -144,7 +135,6 @@ pub fn guess_mime_from_path<P: AsRef<Path>>(path: P) -> &'static str {
|
||||||
mime_guess::from_ext(extension).first_raw().unwrap_or("application/octet-stream")
|
mime_guess::from_ext(extension).first_raw().unwrap_or("application/octet-stream")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature="serve_dir")]
|
|
||||||
/// Print a warning to the log asking to file an issue and respond with "Unexpected Error"
|
/// Print a warning to the log asking to file an issue and respond with "Unexpected Error"
|
||||||
pub (crate) fn warn_unexpected(err: impl std::fmt::Debug, path: &Path, line: u32) -> Response {
|
pub (crate) fn warn_unexpected(err: impl std::fmt::Debug, path: &Path, line: u32) -> Response {
|
||||||
warn!(
|
warn!(
|
||||||
|
@ -159,19 +149,3 @@ pub (crate) fn warn_unexpected(err: impl std::fmt::Debug, path: &Path, line: u32
|
||||||
);
|
);
|
||||||
Response::temporary_failure("Unexpected error")
|
Response::temporary_failure("Unexpected error")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A convenience trait alias for `AsRef<T> + Into<T::Owned>`,
|
|
||||||
/// most commonly used to accept `&str` or `String`:
|
|
||||||
///
|
|
||||||
/// `Cowy<str>` ⇔ `AsRef<str> + Into<String>`
|
|
||||||
pub trait Cowy<T>
|
|
||||||
where
|
|
||||||
Self: AsRef<T> + Into<T::Owned>,
|
|
||||||
T: ToOwned + ?Sized,
|
|
||||||
{}
|
|
||||||
|
|
||||||
impl<C, T> Cowy<T> for C
|
|
||||||
where
|
|
||||||
C: AsRef<T> + Into<T::Owned>,
|
|
||||||
T: ToOwned + ?Sized,
|
|
||||||
{}
|
|
|
@ -13,7 +13,9 @@ use std::{
|
||||||
#[cfg(feature = "serve_dir")]
|
#[cfg(feature = "serve_dir")]
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::{Document, types::{Body, Response, Request}};
|
use crate::{Body, Response, Request};
|
||||||
|
#[cfg(feature = "gemtext")]
|
||||||
|
use crate::Gemtext;
|
||||||
|
|
||||||
/// A struct representing something capable of handling a request.
|
/// A struct representing something capable of handling a request.
|
||||||
///
|
///
|
||||||
|
@ -48,11 +50,11 @@ pub enum Handler {
|
||||||
/// For serving files & directories, try looking at creating a [`FilesHandler`] by
|
/// For serving files & directories, try looking at creating a [`FilesHandler`] by
|
||||||
/// [passing a directory](#impl-From<PathBuf>).
|
/// [passing a directory](#impl-From<PathBuf>).
|
||||||
///
|
///
|
||||||
/// Most often created by using [`From<Response>`] or [`From<Document>`]
|
/// Most often created by using [`From<Response>`] or [`From<Gemtext>`]
|
||||||
///
|
///
|
||||||
/// [`FilesHandler`]: Self::FilesHandler
|
/// [`FilesHandler`]: Self::FilesHandler
|
||||||
/// [`From<Response>`]: #impl-From<Response>
|
/// [`From<Response>`]: #impl-From<Response>
|
||||||
/// [`From<Document>`]: #impl-From<%26'_%20Document>
|
/// [`From<Gemtext>`]: #impl-From<Gemtext>
|
||||||
StaticHandler(Response),
|
StaticHandler(Response),
|
||||||
|
|
||||||
#[cfg(feature = "serve_dir")]
|
#[cfg(feature = "serve_dir")]
|
||||||
|
@ -192,7 +194,8 @@ impl From<Response> for Handler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&Document> for Handler {
|
#[cfg(feature = "gemtext")]
|
||||||
|
impl From<Gemtext> for Handler {
|
||||||
/// Serve an unchanging response, shorthand for From<Response>
|
/// Serve an unchanging response, shorthand for From<Response>
|
||||||
///
|
///
|
||||||
/// This document will be sent in response to any requests that arrive at this
|
/// This document will be sent in response to any requests that arrive at this
|
||||||
|
@ -202,7 +205,7 @@ impl From<&Document> for Handler {
|
||||||
/// This will create a [`StaticHandler`]
|
/// This will create a [`StaticHandler`]
|
||||||
///
|
///
|
||||||
/// [`StaticHandler`]: Self::StaticHandler
|
/// [`StaticHandler`]: Self::StaticHandler
|
||||||
fn from(doc: &Document) -> Self {
|
fn from(doc: Gemtext) -> Self {
|
||||||
Self::StaticHandler(doc.into())
|
Self::StaticHandler(doc.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
12
src/lib.rs
12
src/lib.rs
|
@ -23,6 +23,10 @@
|
||||||
//! users can access certain areas of an application. This is primarily configured using
|
//! users can access certain areas of an application. This is primarily configured using
|
||||||
//! the [`Server::ratelimit()`] method.
|
//! the [`Server::ratelimit()`] method.
|
||||||
//!
|
//!
|
||||||
|
//! * `gemtext` - Adds in integration with the `gemtext` crate, allowing easily creating
|
||||||
|
//! responses using a builder pattern. Please see the `document` example for a
|
||||||
|
//! demonstration. This is implied by `serve_dir` or `user_management_routes`
|
||||||
|
//!
|
||||||
//! * `serve_dir` - Adds in utilities for serving files & directories from the disk at
|
//! * `serve_dir` - Adds in utilities for serving files & directories from the disk at
|
||||||
//! runtime. The easiest way to use this is to pass a [`PathBuf`] to the
|
//! runtime. The easiest way to use this is to pass a [`PathBuf`] to the
|
||||||
//! [`Server::add_route()`] method, which will either serve a directory or a single file.
|
//! [`Server::add_route()`] method, which will either serve a directory or a single file.
|
||||||
|
@ -231,7 +235,8 @@ use rustls::Session;
|
||||||
|
|
||||||
mod types;
|
mod types;
|
||||||
mod handling;
|
mod handling;
|
||||||
pub mod util;
|
#[cfg(feature = "serve_dir")]
|
||||||
|
pub mod files;
|
||||||
pub mod routing;
|
pub mod routing;
|
||||||
#[cfg(feature = "ratelimiting")]
|
#[cfg(feature = "ratelimiting")]
|
||||||
mod ratelimiting;
|
mod ratelimiting;
|
||||||
|
@ -249,6 +254,11 @@ pub use uriparse::URIReference;
|
||||||
pub use types::*;
|
pub use types::*;
|
||||||
pub use handling::Handler;
|
pub use handling::Handler;
|
||||||
|
|
||||||
|
#[cfg(feature = "gemtext")]
|
||||||
|
#[doc(no_inline)]
|
||||||
|
/// A re-export of [`gemtext::Builder`], used for building `text/gemini` documents
|
||||||
|
pub use gemtext::Builder as Gemtext;
|
||||||
|
|
||||||
/// The maximun length of a Request URI
|
/// The maximun length of a Request URI
|
||||||
pub const REQUEST_URI_MAX_LEN: usize = 1024;
|
pub const REQUEST_URI_MAX_LEN: usize = 1024;
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,3 @@ pub use response::Response;
|
||||||
|
|
||||||
mod body;
|
mod body;
|
||||||
pub use body::Body;
|
pub use body::Body;
|
||||||
|
|
||||||
pub mod document;
|
|
||||||
pub use document::Document;
|
|
||||||
|
|
|
@ -4,9 +4,8 @@ use tokio::io::AsyncReadExt;
|
||||||
#[cfg(feature="serve_dir")]
|
#[cfg(feature="serve_dir")]
|
||||||
use tokio::fs::File;
|
use tokio::fs::File;
|
||||||
|
|
||||||
use std::borrow::Borrow;
|
#[cfg(feature = "gemtext")]
|
||||||
|
use crate::Gemtext;
|
||||||
use crate::types::Document;
|
|
||||||
|
|
||||||
/// The body of a response
|
/// The body of a response
|
||||||
///
|
///
|
||||||
|
@ -70,9 +69,27 @@ impl Body {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<D: Borrow<Document>> From<D> for Body {
|
#[cfg(feature = "gemtext")]
|
||||||
fn from(document: D) -> Self {
|
#[allow(clippy::fallible_impl_from)] // It's really not fallible but thanks
|
||||||
Self::from(document.borrow().to_string())
|
impl From<Vec<gemtext::Node>> for Body {
|
||||||
|
/// Render a series of [`gemtext`] nodes to a `text/gemini` body without [normalizing]
|
||||||
|
///
|
||||||
|
/// [normalizing]: Gemtext::normalize
|
||||||
|
fn from(document: Vec<gemtext::Node>) -> Self {
|
||||||
|
let size: usize = document.iter().map(gemtext::Node::estimate_len).sum();
|
||||||
|
let mut bytes = Vec::with_capacity(size + document.len());
|
||||||
|
gemtext::render(document, &mut bytes).unwrap(); // Safe: we're only writing to a buffer
|
||||||
|
Self::Bytes(bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "gemtext")]
|
||||||
|
impl From<Gemtext> for Body {
|
||||||
|
/// [Normalize][1] & eender a series of [`gemtext`] nodes to a `text/gemini` body
|
||||||
|
///
|
||||||
|
/// [1]: Gemtext::normalize
|
||||||
|
fn from(document: Gemtext) -> Self {
|
||||||
|
document.normalize().build().into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,558 +0,0 @@
|
||||||
//! Provides types for creating Gemini Documents.
|
|
||||||
//!
|
|
||||||
//! The module is centered around the `Document` type,
|
|
||||||
//! which provides all the necessary methods for programatically
|
|
||||||
//! creation of Gemini documents.
|
|
||||||
//!
|
|
||||||
//! # Examples
|
|
||||||
//!
|
|
||||||
//! ```
|
|
||||||
//! use kochab::document::HeadingLevel::*;
|
|
||||||
//!
|
|
||||||
//! let mut document = kochab::Document::new();
|
|
||||||
//!
|
|
||||||
//! document.add_heading(H1, "Heading 1");
|
|
||||||
//! document.add_heading(H2, "Heading 2");
|
|
||||||
//! document.add_heading(H3, "Heading 3");
|
|
||||||
//! document.add_blank_line();
|
|
||||||
//! document.add_text("text");
|
|
||||||
//! document.add_link("gemini://gemini.circumlunar.space", "Project Gemini");
|
|
||||||
//! document.add_unordered_list_item("list item");
|
|
||||||
//! document.add_quote("quote");
|
|
||||||
//! document.add_preformatted("preformatted");
|
|
||||||
//!
|
|
||||||
//! assert_eq!(document.to_string(), "\
|
|
||||||
//! ## Heading 1\n\
|
|
||||||
//! ### Heading 2\n\
|
|
||||||
//! #### Heading 3\n\
|
|
||||||
//! \n\
|
|
||||||
//! text\n\
|
|
||||||
//! => gemini://gemini.circumlunar.space/ Project Gemini\n\
|
|
||||||
//! * list item\n\
|
|
||||||
//! > quote\n\
|
|
||||||
//! ```\n\
|
|
||||||
//! preformatted\n\
|
|
||||||
//! ```\n\
|
|
||||||
//! ");
|
|
||||||
//! ```
|
|
||||||
#![warn(missing_docs)]
|
|
||||||
use std::convert::TryInto;
|
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
use crate::URIReference;
|
|
||||||
use crate::util::Cowy;
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
/// Represents a Gemini document.
|
|
||||||
///
|
|
||||||
/// Provides convenient methods for programatically
|
|
||||||
/// creation of Gemini documents.
|
|
||||||
pub struct Document {
|
|
||||||
items: Vec<Item>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Document {
|
|
||||||
/// Creates an empty Gemini `Document`.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let document = kochab::Document::new();
|
|
||||||
///
|
|
||||||
/// assert_eq!(document.to_string(), "");
|
|
||||||
/// ```
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds an `item` to the document.
|
|
||||||
///
|
|
||||||
/// An `item` usually corresponds to a single line,
|
|
||||||
/// except in the case of preformatted text.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```compile_fail
|
|
||||||
/// use kochab::document::{Document, Item, Text};
|
|
||||||
///
|
|
||||||
/// let mut document = Document::new();
|
|
||||||
/// let text = Text::new_lossy("foo");
|
|
||||||
/// let item = Item::Text(text);
|
|
||||||
///
|
|
||||||
/// document.add_item(item);
|
|
||||||
///
|
|
||||||
/// assert_eq!(document.to_string(), "foo\n");
|
|
||||||
/// ```
|
|
||||||
fn add_item(&mut self, item: Item) -> &mut Self {
|
|
||||||
self.items.push(item);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds multiple `items` to the document.
|
|
||||||
///
|
|
||||||
/// This is a convenience wrapper around `add_item`.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```compile_fail
|
|
||||||
/// use kochab::document::{Document, Item, Text};
|
|
||||||
///
|
|
||||||
/// let mut document = Document::new();
|
|
||||||
/// let items = vec!["foo", "bar", "baz"]
|
|
||||||
/// .into_iter()
|
|
||||||
/// .map(Text::new_lossy)
|
|
||||||
/// .map(Item::Text);
|
|
||||||
///
|
|
||||||
/// document.add_items(items);
|
|
||||||
///
|
|
||||||
/// assert_eq!(document.to_string(), "foo\nbar\nbaz\n");
|
|
||||||
/// ```
|
|
||||||
fn add_items<I>(&mut self, items: I) -> &mut Self
|
|
||||||
where
|
|
||||||
I: IntoIterator<Item = Item>,
|
|
||||||
{
|
|
||||||
self.items.extend(items);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a blank line to the document.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let mut document = kochab::Document::new();
|
|
||||||
///
|
|
||||||
/// document.add_blank_line();
|
|
||||||
///
|
|
||||||
/// assert_eq!(document.to_string(), "\n");
|
|
||||||
/// ```
|
|
||||||
pub fn add_blank_line(&mut self) -> &mut Self {
|
|
||||||
self.add_item(Item::Text(Text::blank()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds plain text to the document.
|
|
||||||
///
|
|
||||||
/// This function allows adding multiple lines at once.
|
|
||||||
///
|
|
||||||
/// It inserts a whitespace at the beginning of a line
|
|
||||||
/// if it starts with a character sequence that
|
|
||||||
/// would make it a non-plain text line (e.g. link, heading etc).
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let mut document = kochab::Document::new();
|
|
||||||
///
|
|
||||||
/// document.add_text("hello\n* world!");
|
|
||||||
///
|
|
||||||
/// assert_eq!(document.to_string(), "hello\n * world!\n");
|
|
||||||
/// ```
|
|
||||||
pub fn add_text(&mut self, text: impl AsRef<str>) -> &mut Self {
|
|
||||||
let text = text
|
|
||||||
.as_ref()
|
|
||||||
.lines()
|
|
||||||
.map(Text::new_lossy)
|
|
||||||
.map(Item::Text);
|
|
||||||
|
|
||||||
self.add_items(text);
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a link to the document.
|
|
||||||
///
|
|
||||||
/// `uri`s that fail to parse are substituted with `.`.
|
|
||||||
///
|
|
||||||
/// Consecutive newlines in `label` will be replaced
|
|
||||||
/// with a single whitespace.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let mut document = kochab::Document::new();
|
|
||||||
///
|
|
||||||
/// document.add_link("https://wikipedia.org", "Wiki\n\nWiki");
|
|
||||||
///
|
|
||||||
/// assert_eq!(document.to_string(), "=> https://wikipedia.org/ Wiki Wiki\n");
|
|
||||||
/// ```
|
|
||||||
pub fn add_link<'a, U>(&mut self, uri: U, label: impl Cowy<str>) -> &mut Self
|
|
||||||
where
|
|
||||||
U: TryInto<URIReference<'a>>,
|
|
||||||
{
|
|
||||||
let uri = uri
|
|
||||||
.try_into()
|
|
||||||
.map(URIReference::into_owned)
|
|
||||||
.or_else(|_| ".".try_into()).expect("Northstar BUG");
|
|
||||||
let label = LinkLabel::from_lossy(label);
|
|
||||||
let link = Link { uri: Box::new(uri), label: Some(label) };
|
|
||||||
let link = Item::Link(link);
|
|
||||||
|
|
||||||
self.add_item(link);
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a link to the document, but without a label.
|
|
||||||
///
|
|
||||||
/// See `add_link` for details.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let mut document = kochab::Document::new();
|
|
||||||
///
|
|
||||||
/// document.add_link_without_label("https://wikipedia.org");
|
|
||||||
///
|
|
||||||
/// assert_eq!(document.to_string(), "=> https://wikipedia.org/\n");
|
|
||||||
/// ```
|
|
||||||
pub fn add_link_without_label<'a, U>(&mut self, uri: U) -> &mut Self
|
|
||||||
where
|
|
||||||
U: TryInto<URIReference<'a>>,
|
|
||||||
{
|
|
||||||
let uri = uri
|
|
||||||
.try_into()
|
|
||||||
.map(URIReference::into_owned)
|
|
||||||
.or_else(|_| ".".try_into()).expect("Northstar BUG");
|
|
||||||
let link = Link {
|
|
||||||
uri: Box::new(uri),
|
|
||||||
label: None,
|
|
||||||
};
|
|
||||||
let link = Item::Link(link);
|
|
||||||
|
|
||||||
self.add_item(link);
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a block of preformatted text.
|
|
||||||
///
|
|
||||||
/// Lines that start with ` ``` ` will be prependend with a whitespace.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let mut document = kochab::Document::new();
|
|
||||||
///
|
|
||||||
/// document.add_preformatted("a\n b\n c");
|
|
||||||
///
|
|
||||||
/// assert_eq!(document.to_string(), "```\na\n b\n c\n```\n");
|
|
||||||
/// ```
|
|
||||||
pub fn add_preformatted(&mut self, preformatted_text: impl AsRef<str>) -> &mut Self {
|
|
||||||
self.add_preformatted_with_alt("", preformatted_text.as_ref())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a block of preformatted text with an alt text.
|
|
||||||
///
|
|
||||||
/// Consecutive newlines in `alt` will be replaced
|
|
||||||
/// with a single whitespace.
|
|
||||||
///
|
|
||||||
/// `preformatted_text` lines that start with ` ``` `
|
|
||||||
/// will be prependend with a whitespace.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let mut document = kochab::Document::new();
|
|
||||||
///
|
|
||||||
/// document.add_preformatted_with_alt("rust", "fn main() {\n}\n");
|
|
||||||
///
|
|
||||||
/// assert_eq!(document.to_string(), "```rust\nfn main() {\n}\n```\n");
|
|
||||||
/// ```
|
|
||||||
pub fn add_preformatted_with_alt(&mut self, alt: impl AsRef<str>, preformatted_text: impl AsRef<str>) -> &mut Self {
|
|
||||||
let alt = AltText::new_lossy(alt.as_ref());
|
|
||||||
let lines = preformatted_text
|
|
||||||
.as_ref()
|
|
||||||
.lines()
|
|
||||||
.map(PreformattedText::new_lossy)
|
|
||||||
.collect();
|
|
||||||
let preformatted = Preformatted {
|
|
||||||
alt,
|
|
||||||
lines,
|
|
||||||
};
|
|
||||||
let preformatted = Item::Preformatted(preformatted);
|
|
||||||
|
|
||||||
self.add_item(preformatted);
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a heading.
|
|
||||||
///
|
|
||||||
/// Consecutive newlines in `text` will be replaced
|
|
||||||
/// with a single whitespace.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use kochab::document::HeadingLevel::H1;
|
|
||||||
///
|
|
||||||
/// let mut document = kochab::Document::new();
|
|
||||||
///
|
|
||||||
/// document.add_heading(H1, "Welcome!");
|
|
||||||
///
|
|
||||||
/// assert_eq!(document.to_string(), "# Welcome!\n");
|
|
||||||
/// ```
|
|
||||||
pub fn add_heading(&mut self, level: HeadingLevel, text: impl Cowy<str>) -> &mut Self {
|
|
||||||
let text = HeadingText::new_lossy(text);
|
|
||||||
let heading = Heading {
|
|
||||||
level,
|
|
||||||
text,
|
|
||||||
};
|
|
||||||
let heading = Item::Heading(heading);
|
|
||||||
|
|
||||||
self.add_item(heading);
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds an unordered list item.
|
|
||||||
///
|
|
||||||
/// Consecutive newlines in `text` will be replaced
|
|
||||||
/// with a single whitespace.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let mut document = kochab::Document::new();
|
|
||||||
///
|
|
||||||
/// document.add_unordered_list_item("milk");
|
|
||||||
/// document.add_unordered_list_item("eggs");
|
|
||||||
///
|
|
||||||
/// assert_eq!(document.to_string(), "* milk\n* eggs\n");
|
|
||||||
/// ```
|
|
||||||
pub fn add_unordered_list_item(&mut self, text: impl AsRef<str>) -> &mut Self {
|
|
||||||
let item = UnorderedListItem::new_lossy(text.as_ref());
|
|
||||||
let item = Item::UnorderedListItem(item);
|
|
||||||
|
|
||||||
self.add_item(item);
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a quote.
|
|
||||||
///
|
|
||||||
/// This function allows adding multiple quote lines at once.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let mut document = kochab::Document::new();
|
|
||||||
///
|
|
||||||
/// document.add_quote("I think,\ntherefore I am");
|
|
||||||
///
|
|
||||||
/// assert_eq!(document.to_string(), "> I think,\n> therefore I am\n");
|
|
||||||
/// ```
|
|
||||||
pub fn add_quote(&mut self, text: impl AsRef<str>) -> &mut Self {
|
|
||||||
let quote = text
|
|
||||||
.as_ref()
|
|
||||||
.lines()
|
|
||||||
.map(Quote::new_lossy)
|
|
||||||
.map(Item::Quote);
|
|
||||||
|
|
||||||
self.add_items(quote);
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Document {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
for item in &self.items {
|
|
||||||
match item {
|
|
||||||
Item::Text(text) => writeln!(f, "{}", text.0)?,
|
|
||||||
Item::Link(link) => {
|
|
||||||
let separator = if link.label.is_some() {" "} else {""};
|
|
||||||
let label = link.label.as_ref().map(|label| label.0.as_str())
|
|
||||||
.unwrap_or("");
|
|
||||||
|
|
||||||
writeln!(f, "=> {}{}{}", link.uri, separator, label)?;
|
|
||||||
}
|
|
||||||
Item::Preformatted(preformatted) => {
|
|
||||||
writeln!(f, "```{}", preformatted.alt.0)?;
|
|
||||||
|
|
||||||
for line in &preformatted.lines {
|
|
||||||
writeln!(f, "{}", line.0)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
writeln!(f, "```")?
|
|
||||||
}
|
|
||||||
Item::Heading(heading) => {
|
|
||||||
let level = match heading.level {
|
|
||||||
HeadingLevel::H1 => "#",
|
|
||||||
HeadingLevel::H2 => "##",
|
|
||||||
HeadingLevel::H3 => "###",
|
|
||||||
};
|
|
||||||
|
|
||||||
writeln!(f, "{} {}", level, heading.text.0)?;
|
|
||||||
}
|
|
||||||
Item::UnorderedListItem(item) => writeln!(f, "* {}", item.0)?,
|
|
||||||
Item::Quote(quote) => writeln!(f, "> {}", quote.0)?,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::enum_variant_names)]
|
|
||||||
enum Item {
|
|
||||||
Text(Text),
|
|
||||||
Link(Link),
|
|
||||||
Preformatted(Preformatted),
|
|
||||||
Heading(Heading),
|
|
||||||
UnorderedListItem(UnorderedListItem),
|
|
||||||
Quote(Quote),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct Text(String);
|
|
||||||
|
|
||||||
impl Text {
|
|
||||||
fn blank() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new_lossy(line: impl Cowy<str>) -> Self {
|
|
||||||
Self(lossy_escaped_line(line, SPECIAL_STARTS))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Link {
|
|
||||||
uri: Box<URIReference<'static>>,
|
|
||||||
label: Option<LinkLabel>,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct LinkLabel(String);
|
|
||||||
|
|
||||||
impl LinkLabel {
|
|
||||||
fn from_lossy(line: impl Cowy<str>) -> Self {
|
|
||||||
let line = strip_newlines(line);
|
|
||||||
|
|
||||||
Self(line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Preformatted {
|
|
||||||
alt: AltText,
|
|
||||||
lines: Vec<PreformattedText>,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct PreformattedText(String);
|
|
||||||
|
|
||||||
impl PreformattedText {
|
|
||||||
fn new_lossy(line: impl Cowy<str>) -> Self {
|
|
||||||
Self(lossy_escaped_line(line, &[PREFORMATTED_TOGGLE_START]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AltText(String);
|
|
||||||
|
|
||||||
impl AltText {
|
|
||||||
fn new_lossy(alt: &str) -> Self {
|
|
||||||
let alt = strip_newlines(alt);
|
|
||||||
|
|
||||||
Self(alt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Heading {
|
|
||||||
level: HeadingLevel,
|
|
||||||
text: HeadingText,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The level of a heading.
|
|
||||||
pub enum HeadingLevel {
|
|
||||||
/// Heading level 1 (`#`)
|
|
||||||
H1,
|
|
||||||
/// Heading level 2 (`##`)
|
|
||||||
H2,
|
|
||||||
/// Heading level 3 (`###`)
|
|
||||||
H3,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct HeadingText(String);
|
|
||||||
|
|
||||||
impl HeadingText {
|
|
||||||
fn new_lossy(line: impl Cowy<str>) -> Self {
|
|
||||||
let line = strip_newlines(line);
|
|
||||||
|
|
||||||
Self(line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct UnorderedListItem(String);
|
|
||||||
|
|
||||||
impl UnorderedListItem {
|
|
||||||
fn new_lossy(text: &str) -> Self {
|
|
||||||
let text = strip_newlines(text);
|
|
||||||
|
|
||||||
Self(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Quote(String);
|
|
||||||
|
|
||||||
impl Quote {
|
|
||||||
fn new_lossy(text: &str) -> Self {
|
|
||||||
Self(lossy_escaped_line(text, &[QUOTE_START]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const LINK_START: &str = "=>";
|
|
||||||
const PREFORMATTED_TOGGLE_START: &str = "```";
|
|
||||||
const HEADING_START: &str = "#";
|
|
||||||
const UNORDERED_LIST_ITEM_START: &str = "*";
|
|
||||||
const QUOTE_START: &str = ">";
|
|
||||||
|
|
||||||
const SPECIAL_STARTS: &[&str] = &[
|
|
||||||
LINK_START,
|
|
||||||
PREFORMATTED_TOGGLE_START,
|
|
||||||
HEADING_START,
|
|
||||||
UNORDERED_LIST_ITEM_START,
|
|
||||||
QUOTE_START,
|
|
||||||
];
|
|
||||||
|
|
||||||
fn starts_with_any(s: &str, starts: &[&str]) -> bool {
|
|
||||||
for start in starts {
|
|
||||||
if s.starts_with(start) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn lossy_escaped_line(line: impl Cowy<str>, escape_starts: &[&str]) -> String {
|
|
||||||
let line_ref = line.as_ref();
|
|
||||||
let contains_newline = line_ref.contains('\n');
|
|
||||||
let has_special_start = starts_with_any(line_ref, escape_starts);
|
|
||||||
|
|
||||||
if !contains_newline && !has_special_start {
|
|
||||||
return line.into();
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut line = String::new();
|
|
||||||
|
|
||||||
if has_special_start {
|
|
||||||
line.push(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(line_ref) = line_ref.split('\n').next() {
|
|
||||||
line.push_str(line_ref);
|
|
||||||
}
|
|
||||||
|
|
||||||
line
|
|
||||||
}
|
|
||||||
|
|
||||||
fn strip_newlines(text: impl Cowy<str>) -> String {
|
|
||||||
if !text.as_ref().contains(&['\r', '\n'][..]) {
|
|
||||||
return text.into();
|
|
||||||
}
|
|
||||||
|
|
||||||
text.as_ref()
|
|
||||||
.lines()
|
|
||||||
.filter(|part| !part.is_empty())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(" ")
|
|
||||||
}
|
|
|
@ -1,6 +1,4 @@
|
||||||
use std::borrow::Borrow;
|
use crate::types::Body;
|
||||||
|
|
||||||
use crate::types::{Body, Document};
|
|
||||||
|
|
||||||
/// A response to a client's [`Request`]
|
/// A response to a client's [`Request`]
|
||||||
///
|
///
|
||||||
|
@ -248,8 +246,9 @@ impl AsMut<Option<Body>> for Response {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<D: Borrow<Document>> From<D> for Response {
|
#[cfg(feature = "gemtext")]
|
||||||
|
impl<D: Into<Vec<gemtext::Node>>> From<D> for Response {
|
||||||
fn from(doc: D) -> Self {
|
fn from(doc: D) -> Self {
|
||||||
Self::success_gemini(doc)
|
Self::success_gemini(doc.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,7 @@ use std::sync::RwLock;
|
||||||
|
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
|
|
||||||
use crate::{Document, Request, Response};
|
use crate::{Gemtext, Request, Response};
|
||||||
use crate::types::document::HeadingLevel;
|
|
||||||
use crate::user_management::{
|
use crate::user_management::{
|
||||||
User,
|
User,
|
||||||
RegisteredUser,
|
RegisteredUser,
|
||||||
|
@ -353,34 +352,45 @@ async fn handle_password<UserData: Serialize + DeserializeOwned + Default>(reque
|
||||||
fn render_settings_menu<UserData: Serialize + DeserializeOwned>(
|
fn render_settings_menu<UserData: Serialize + DeserializeOwned>(
|
||||||
user: RegisteredUser<UserData>
|
user: RegisteredUser<UserData>
|
||||||
) -> Response {
|
) -> Response {
|
||||||
let mut document = Document::new();
|
#[cfg_attr(not(feature = "user_management_advanced"), allow(unused_mut))]
|
||||||
document
|
let mut document = Gemtext::new()
|
||||||
.add_heading(HeadingLevel::H1, "User Settings")
|
.heading(1, "User Settings")
|
||||||
.add_blank_line()
|
.blank_line()
|
||||||
.add_text(&format!("Welcome {}!", user.username()))
|
.text(&format!("Welcome {}!", user.username()))
|
||||||
.add_blank_line()
|
.blank_line()
|
||||||
.add_link(get_redirect(&user).as_str(), "Back to the app")
|
.link(get_redirect(&user).as_str(), Some("Back to the app".to_string()))
|
||||||
.add_blank_line();
|
.blank_line();
|
||||||
|
|
||||||
#[cfg(feature = "user_management_advanced")]
|
#[cfg(feature = "user_management_advanced")] {
|
||||||
document
|
document = document
|
||||||
.add_text(
|
.text(
|
||||||
if user.has_password() {
|
if user.has_password() {
|
||||||
concat!(
|
concat!(
|
||||||
"You currently have a password set. This can be used to link any new",
|
"You currently have a password set. This can be used to link any new",
|
||||||
" certificates or clients to your account. If you don't remember your",
|
" certificates or clients to your account. If you don't remember your",
|
||||||
" password, or would like to change it, you may do so here.",
|
" password, or would like to change it, you may do so here.",
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
concat!(
|
||||||
|
"You don't currently have a password set! Without a password, you cannot",
|
||||||
|
" link any new certificates to your account, and if you lose your current",
|
||||||
|
" client or certificate, you won't be able to recover your account.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.blank_line()
|
||||||
|
.link(
|
||||||
|
"/account/password",
|
||||||
|
Some(
|
||||||
|
if user.has_password() {
|
||||||
|
"Change password"
|
||||||
|
} else {
|
||||||
|
"Set password"
|
||||||
|
}
|
||||||
|
.to_string()
|
||||||
)
|
)
|
||||||
} else {
|
);
|
||||||
concat!(
|
}
|
||||||
"You don't currently have a password set! Without a password, you cannot",
|
|
||||||
" link any new certificates to your account, and if you lose your current",
|
|
||||||
" client or certificate, you won't be able to recover your account.",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.add_blank_line()
|
|
||||||
.add_link("/account/password", if user.has_password() { "Change password" } else { "Set password" });
|
|
||||||
|
|
||||||
document.into()
|
document.into()
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
||||||
use sled::Transactional;
|
use sled::Transactional;
|
||||||
|
|
||||||
#[cfg(not(feature = "ring"))]
|
#[cfg(all(not(feature = "ring"), feature = "user_management_advanced"))]
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use crate::user_management::UserManager;
|
use crate::user_management::UserManager;
|
||||||
|
|
Loading…
Reference in a new issue