implement document API

gemtext
panicbit 2020-11-14 09:55:21 +01:00
parent 75a1cd1eb5
commit 0c05d6d162
7 changed files with 418 additions and 17 deletions

50
examples/document.rs Normal file
View File

@ -0,0 +1,50 @@
use anyhow::*;
use futures::{future::BoxFuture, FutureExt};
use log::LevelFilter;
use northstar::{Server, Request, Response, GEMINI_PORT, Document};
use northstar::document::HeadingLevel::*;
#[tokio::main]
async fn main() -> Result<()> {
env_logger::builder()
.filter_module("northstar", LevelFilter::Debug)
.init();
Server::bind(("localhost", GEMINI_PORT))
.serve(handle_request)
.await
}
fn handle_request(_request: Request) -> BoxFuture<'static, Result<Response>> {
async move {
let mut document = Document::new();
document
.add_preformatted(include_str!("northstar_logo.txt"))
.add_blank_line()
.add_link("https://docs.rs/northstar", "Documentation")
.add_link("https://github.com/panicbit/northstar", "GitHub")
.add_blank_line()
.add_heading(H1, "Usage")
.add_blank_line()
.add_text("Add the latest version of northstar to your `Cargo.toml`.")
.add_blank_line()
.add_heading(H2, "Manually")
.add_blank_line()
.add_preformatted_with_alt("alt", r#"northstar = "0.3.0" # check crates.io for the latest version"#)
.add_blank_line()
.add_heading(H2, "Automatically")
.add_blank_line()
.add_preformatted_with_alt("sh", "cargo add northstar")
.add_blank_line()
.add_heading(H1, "Generating a key & certificate")
.add_blank_line()
.add_preformatted_with_alt("sh", concat!(
"mkdir cert && cd cert\n",
"openssl req -x509 -nodes -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365",
));
Ok(Response::document(document))
}
.boxed()
}

View File

@ -0,0 +1,5 @@
__ __ __
____ ____ _____/ /_/ /_ _____/ /_____ ______
/ __ \/ __ \/ ___/ __/ __ \/ ___/ __/ __ `/ ___/
/ / / / /_/ / / / /_/ / / (__ ) /_/ /_/ / /
/_/ /_/\____/_/ \__/_/ /_/____/\__/\__,_/_/

View File

@ -1,5 +1,6 @@
pub use ::mime::Mime;
pub use rustls::Certificate;
pub use uriparse::URIReference;
mod meta;
pub use self::meta::Meta;
@ -18,3 +19,6 @@ pub use response::Response;
mod body;
pub use body::Body;
pub mod document;
pub use document::Document;

View File

@ -1,10 +1,18 @@
use tokio::{io::AsyncRead, fs::File};
use crate::types::Document;
pub enum Body {
Bytes(Vec<u8>),
Reader(Box<dyn AsyncRead + Send + Sync + Unpin>),
}
impl From<Document> for Body {
fn from(document: Document) -> Self {
Body::from(document.to_string())
}
}
impl From<Vec<u8>> for Body {
fn from(bytes: Vec<u8>) -> Self {
Self::Bytes(bytes)

332
src/types/document.rs Normal file
View File

@ -0,0 +1,332 @@
use std::convert::TryInto;
use std::fmt;
use itertools::Itertools;
use crate::types::URIReference;
#[derive(Default)]
pub struct Document {
items: Vec<Item>,
}
impl Document {
pub fn new() -> Self {
Self::default()
}
pub fn add_item(&mut self, item: Item) -> &mut Self {
self.items.push(item);
self
}
pub fn add_items<I>(&mut self, items: I) -> &mut Self
where
I: IntoIterator<Item = Item>,
{
self.items.extend(items);
self
}
pub fn add_blank_line(&mut self) -> &mut Self {
self.add_item(Item::Text(Text::blank()))
}
pub fn add_text(&mut self, text: &str) -> &mut Self {
let text = text
.lines()
.map(Text::new_lossy)
.map(Item::Text);
self.add_items(text);
self
}
pub fn add_link<'a, U>(&mut self, uri: U, label: impl AsRef<str> + Into<String>) -> &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, label: Some(label) };
let link = Item::Link(link);
self.add_item(link);
self
}
pub fn add_link_without_label(&mut self, uri: URIReference<'static>) -> &mut Self {
let link = Link {
uri,
label: None,
};
let link = Item::Link(link);
self.add_item(link);
self
}
pub fn add_preformatted(&mut self, preformatted_text: &str) -> &mut Self {
self.add_preformatted_with_alt("", preformatted_text)
}
pub fn add_preformatted_with_alt(&mut self, alt: &str, preformatted_text: &str) -> &mut Self {
let alt = AltText::new_lossy(alt);
let lines = preformatted_text
.lines()
.map(PreformattedText::new_lossy)
.collect();
let preformatted = Preformatted {
alt,
lines,
};
let preformatted = Item::Preformatted(preformatted);
self.add_item(preformatted);
self
}
pub fn add_heading(&mut self, level: HeadingLevel, text: impl AsRef<str> + Into<String>) -> &mut Self {
let text = HeadingText::new_lossy(text);
let heading = Heading {
level,
text,
};
let heading = Item::Heading(heading);
self.add_item(heading);
self
}
pub fn add_unordered_list_item(&mut self, text: &str) -> &mut Self {
let item = UnorderedListItem::new_lossy(text);
let item = Item::UnorderedListItem(item);
self.add_item(item);
self
}
pub fn add_quote(&mut self, text: &str) -> &mut Self {
let quote = text
.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(())
}
}
pub enum Item {
Text(Text),
Link(Link),
Preformatted(Preformatted),
Heading(Heading),
UnorderedListItem(UnorderedListItem),
Quote(Quote),
}
#[derive(Default)]
pub struct Text(String);
impl Text {
pub fn blank() -> Self {
Self::default()
}
pub fn new_lossy(line: impl AsRef<str> + Into<String>) -> Self {
Self(lossy_escaped_line(line, SPECIAL_STARTS))
}
}
pub struct Link {
pub uri: URIReference<'static>,
pub label: Option<LinkLabel>,
}
pub struct LinkLabel(String);
impl LinkLabel {
pub fn from_lossy(line: impl AsRef<str> + Into<String>) -> Self {
let line = strip_newlines(line);
LinkLabel(line)
}
}
pub struct Preformatted {
pub alt: AltText,
pub lines: Vec<PreformattedText>,
}
pub struct PreformattedText(String);
impl PreformattedText {
pub fn new_lossy(line: impl AsRef<str> + Into<String>) -> Self {
Self(lossy_escaped_line(line, &[PREFORMATTED_TOGGLE_START]))
}
}
pub struct AltText(String);
impl AltText {
pub fn new_lossy(alt: &str) -> Self {
let alt = strip_newlines(alt);
Self(alt)
}
}
pub struct Heading {
pub level: HeadingLevel,
pub text: HeadingText,
}
pub enum HeadingLevel {
H1,
H2,
H3,
}
impl Heading {
pub fn new_lossy(level: HeadingLevel, line: &str) -> Self {
Self {
level,
text: HeadingText::new_lossy(line),
}
}
}
pub struct HeadingText(String);
impl HeadingText {
pub fn new_lossy(line: impl AsRef<str> + Into<String>) -> Self {
let line = strip_newlines(line);
Self(lossy_escaped_line(line, &[HEADING_START]))
}
}
pub struct UnorderedListItem(String);
impl UnorderedListItem {
pub fn new_lossy(text: &str) -> Self {
let text = strip_newlines(text);
Self(text)
}
}
pub struct Quote(String);
impl Quote {
pub 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 AsRef<str> + Into<String>, 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 AsRef<str> + Into<String>) -> String {
if !text.as_ref().contains(&['\r', '\n'][..]) {
return text.into();
}
text.as_ref()
.lines()
.filter(|part| !part.is_empty())
.join(" ")
}

View File

@ -1,5 +1,6 @@
use anyhow::*;
use crate::types::{ResponseHeader, Body, Mime};
use crate::types::{ResponseHeader, Body, Mime, Document};
use crate::GEMINI_MIME;
pub struct Response {
header: ResponseHeader,
@ -14,6 +15,10 @@ impl Response {
}
}
pub fn document(document: Document) -> Self {
Self::success(&GEMINI_MIME).with_body(document)
}
pub fn input(prompt: impl AsRef<str> + Into<String>) -> Result<Self> {
let header = ResponseHeader::input(prompt)?;
Ok(Self::new(header))

View File

@ -1,12 +1,12 @@
use std::path::Path;
use mime::Mime;
use percent_encoding::utf8_percent_encode;
use anyhow::*;
use tokio::{
fs::{self, File},
io,
};
use crate::{GEMINI_MIME, GEMINI_MIME_STR, Response};
use crate::GEMINI_MIME_STR;
use crate::types::{Response, Document, document::HeadingLevel::*};
use itertools::Itertools;
pub async fn serve_file<P: AsRef<Path>>(path: P, mime: &Mime) -> Result<Response> {
@ -49,8 +49,6 @@ pub async fn serve_dir<D: AsRef<Path>, P: AsRef<Path>>(dir: D, virtual_path: &[P
}
async fn serve_dir_listing<P: AsRef<Path>, B: AsRef<Path>>(path: P, virtual_path: &[B]) -> Result<Response> {
use std::fmt::Write;
let mut dir = match fs::read_dir(path).await {
Ok(dir) => dir,
Err(err) => match err.kind() {
@ -60,13 +58,13 @@ async fn serve_dir_listing<P: AsRef<Path>, B: AsRef<Path>>(path: P, virtual_path
};
let breadcrumbs = virtual_path.iter().map(|segment| segment.as_ref().display()).join("/");
let mut listing = String::new();
writeln!(listing, "# Index of /{}", breadcrumbs)?;
writeln!(listing)?;
let mut document = Document::new();
document.add_heading(H1, format!("Index of /{}", breadcrumbs));
document.add_blank_line();
if virtual_path.get(0).map(<_>::as_ref) != Some(Path::new("")) {
writeln!(listing, "=> .. 📁 ../")?;
document.add_link("..", "📁 ../");
}
while let Some(entry) = dir.next_entry().await.context("Failed to list directory")? {
@ -75,18 +73,17 @@ async fn serve_dir_listing<P: AsRef<Path>, B: AsRef<Path>>(path: P, virtual_path
let is_dir = entry.file_type().await
.with_context(|| format!("Failed to get file type of `{}`", entry.path().display()))?
.is_dir();
let trailing_slash = if is_dir { "/" } else { "" };
let uri = format!("./{}{}", file_name, trailing_slash);
writeln!(
listing,
"=> {link}{trailing_slash} {icon} {name}{trailing_slash}",
document.add_link(uri.as_str(), format!("{icon} {name}{trailing_slash}",
icon = if is_dir { '📁' } else { '📄' },
link = utf8_percent_encode(&file_name, percent_encoding::NON_ALPHANUMERIC),
trailing_slash = if is_dir { "/" } else { "" },
name = file_name,
)?;
trailing_slash = trailing_slash
));
}
Ok(Response::success(&GEMINI_MIME).with_body(listing))
Ok(Response::document(document))
}
pub fn guess_mime_from_path<P: AsRef<Path>>(path: P) -> Mime {