From 0c05d6d16259e6cee7cbb200a417a9c6dc9dfe77 Mon Sep 17 00:00:00 2001 From: panicbit Date: Sat, 14 Nov 2020 09:55:21 +0100 Subject: [PATCH 01/10] implement document API --- examples/document.rs | 50 ++++++ examples/northstar_logo.txt | 5 + src/types.rs | 4 + src/types/body.rs | 8 + src/types/document.rs | 332 ++++++++++++++++++++++++++++++++++++ src/types/response.rs | 7 +- src/util.rs | 29 ++-- 7 files changed, 418 insertions(+), 17 deletions(-) create mode 100644 examples/document.rs create mode 100644 examples/northstar_logo.txt create mode 100644 src/types/document.rs diff --git a/examples/document.rs b/examples/document.rs new file mode 100644 index 0000000..8b096f3 --- /dev/null +++ b/examples/document.rs @@ -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> { + 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() +} diff --git a/examples/northstar_logo.txt b/examples/northstar_logo.txt new file mode 100644 index 0000000..9fe390c --- /dev/null +++ b/examples/northstar_logo.txt @@ -0,0 +1,5 @@ + __ __ __ + ____ ____ _____/ /_/ /_ _____/ /_____ ______ + / __ \/ __ \/ ___/ __/ __ \/ ___/ __/ __ `/ ___/ + / / / / /_/ / / / /_/ / / (__ ) /_/ /_/ / / +/_/ /_/\____/_/ \__/_/ /_/____/\__/\__,_/_/ \ No newline at end of file diff --git a/src/types.rs b/src/types.rs index b52d0da..b376427 100644 --- a/src/types.rs +++ b/src/types.rs @@ -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; diff --git a/src/types/body.rs b/src/types/body.rs index 7e697b5..dfeb8ca 100644 --- a/src/types/body.rs +++ b/src/types/body.rs @@ -1,10 +1,18 @@ use tokio::{io::AsyncRead, fs::File}; +use crate::types::Document; + pub enum Body { Bytes(Vec), Reader(Box), } +impl From for Body { + fn from(document: Document) -> Self { + Body::from(document.to_string()) + } +} + impl From> for Body { fn from(bytes: Vec) -> Self { Self::Bytes(bytes) diff --git a/src/types/document.rs b/src/types/document.rs new file mode 100644 index 0000000..e62eecb --- /dev/null +++ b/src/types/document.rs @@ -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, +} + +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(&mut self, items: I) -> &mut Self + where + I: IntoIterator, + { + 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 + Into) -> &mut Self + where + U: TryInto>, + { + 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 + Into) -> &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 + Into) -> Self { + Self(lossy_escaped_line(line, SPECIAL_STARTS)) + } +} + +pub struct Link { + pub uri: URIReference<'static>, + pub label: Option, +} + +pub struct LinkLabel(String); + +impl LinkLabel { + pub fn from_lossy(line: impl AsRef + Into) -> Self { + let line = strip_newlines(line); + + LinkLabel(line) + } +} + +pub struct Preformatted { + pub alt: AltText, + pub lines: Vec, +} + +pub struct PreformattedText(String); + +impl PreformattedText { + pub fn new_lossy(line: impl AsRef + Into) -> 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 + Into) -> 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 + Into, 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 + Into) -> String { + if !text.as_ref().contains(&['\r', '\n'][..]) { + return text.into(); + } + + text.as_ref() + .lines() + .filter(|part| !part.is_empty()) + .join(" ") +} diff --git a/src/types/response.rs b/src/types/response.rs index f2389a0..6a24de7 100644 --- a/src/types/response.rs +++ b/src/types/response.rs @@ -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 + Into) -> Result { let header = ResponseHeader::input(prompt)?; Ok(Self::new(header)) diff --git a/src/util.rs b/src/util.rs index c1265e4..bd7a28c 100644 --- a/src/util.rs +++ b/src/util.rs @@ -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>(path: P, mime: &Mime) -> Result { @@ -49,8 +49,6 @@ pub async fn serve_dir, P: AsRef>(dir: D, virtual_path: &[P } async fn serve_dir_listing, B: AsRef>(path: P, virtual_path: &[B]) -> Result { - 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, B: AsRef>(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, B: AsRef>(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>(path: P) -> Mime { From 2fc015fb89bd67ef39069466ab121912dca0c091 Mon Sep 17 00:00:00 2001 From: panicbit Date: Sat, 14 Nov 2020 10:00:48 +0100 Subject: [PATCH 02/10] fix typo --- examples/document.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/document.rs b/examples/document.rs index 8b096f3..5986896 100644 --- a/examples/document.rs +++ b/examples/document.rs @@ -31,7 +31,7 @@ fn handle_request(_request: Request) -> BoxFuture<'static, Result> { .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_preformatted_with_alt("toml", r#"northstar = "0.3.0" # check crates.io for the latest version"#) .add_blank_line() .add_heading(H2, "Automatically") .add_blank_line() From 0425bf2cf3acfeb4b82c1a8d8d1742a4bfa79f08 Mon Sep 17 00:00:00 2001 From: panicbit Date: Sat, 14 Nov 2020 22:46:29 +0100 Subject: [PATCH 03/10] add Cowy util trait --- src/types/document.rs | 17 +++++++++-------- src/types/meta.rs | 8 +++++--- src/types/response.rs | 7 ++++--- src/types/response_header.rs | 11 ++++++----- src/util.rs | 16 ++++++++++++++++ 5 files changed, 40 insertions(+), 19 deletions(-) diff --git a/src/types/document.rs b/src/types/document.rs index e62eecb..3d65194 100644 --- a/src/types/document.rs +++ b/src/types/document.rs @@ -3,6 +3,7 @@ use std::fmt; use itertools::Itertools; use crate::types::URIReference; +use crate::util::Cowy; #[derive(Default)] pub struct Document { @@ -42,7 +43,7 @@ impl Document { self } - pub fn add_link<'a, U>(&mut self, uri: U, label: impl AsRef + Into) -> &mut Self + pub fn add_link<'a, U>(&mut self, uri: U, label: impl Cowy) -> &mut Self where U: TryInto>, { @@ -92,7 +93,7 @@ impl Document { self } - pub fn add_heading(&mut self, level: HeadingLevel, text: impl AsRef + Into) -> &mut Self { + pub fn add_heading(&mut self, level: HeadingLevel, text: impl Cowy) -> &mut Self { let text = HeadingText::new_lossy(text); let heading = Heading { level, @@ -182,7 +183,7 @@ impl Text { Self::default() } - pub fn new_lossy(line: impl AsRef + Into) -> Self { + pub fn new_lossy(line: impl Cowy) -> Self { Self(lossy_escaped_line(line, SPECIAL_STARTS)) } } @@ -195,7 +196,7 @@ pub struct Link { pub struct LinkLabel(String); impl LinkLabel { - pub fn from_lossy(line: impl AsRef + Into) -> Self { + pub fn from_lossy(line: impl Cowy) -> Self { let line = strip_newlines(line); LinkLabel(line) @@ -210,7 +211,7 @@ pub struct Preformatted { pub struct PreformattedText(String); impl PreformattedText { - pub fn new_lossy(line: impl AsRef + Into) -> Self { + pub fn new_lossy(line: impl Cowy) -> Self { Self(lossy_escaped_line(line, &[PREFORMATTED_TOGGLE_START])) } } @@ -248,7 +249,7 @@ impl Heading { pub struct HeadingText(String); impl HeadingText { - pub fn new_lossy(line: impl AsRef + Into) -> Self { + pub fn new_lossy(line: impl Cowy) -> Self { let line = strip_newlines(line); Self(lossy_escaped_line(line, &[HEADING_START])) @@ -298,7 +299,7 @@ fn starts_with_any(s: &str, starts: &[&str]) -> bool { false } -fn lossy_escaped_line(line: impl AsRef + Into, escape_starts: &[&str]) -> String { +fn lossy_escaped_line(line: impl Cowy, 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); @@ -320,7 +321,7 @@ fn lossy_escaped_line(line: impl AsRef + Into, escape_starts: &[&st line } -fn strip_newlines(text: impl AsRef + Into) -> String { +fn strip_newlines(text: impl Cowy) -> String { if !text.as_ref().contains(&['\r', '\n'][..]) { return text.into(); } diff --git a/src/types/meta.rs b/src/types/meta.rs index 144fc13..ccc17ba 100644 --- a/src/types/meta.rs +++ b/src/types/meta.rs @@ -1,5 +1,7 @@ use anyhow::*; -use mime::Mime; +use crate::Mime; +use crate::util::Cowy; + #[derive(Debug,Clone,PartialEq,Eq,Default)] pub struct Meta(String); @@ -9,7 +11,7 @@ impl Meta { /// Creates a new "Meta" string. /// Fails if `meta` contains `\n`. - pub fn new(meta: impl AsRef + Into) -> Result { + pub fn new(meta: impl Cowy) -> Result { ensure!(!meta.as_ref().contains("\n"), "Meta must not contain newlines"); ensure!(meta.as_ref().len() <= Self::MAX_LEN, "Meta must not exceed {} bytes", Self::MAX_LEN); @@ -20,7 +22,7 @@ impl Meta { /// Truncates `meta` to before: /// - the first occurrence of `\n` /// - the character that makes `meta` exceed `Meta::MAX_LEN` - pub fn new_lossy(meta: impl AsRef + Into) -> Self { + pub fn new_lossy(meta: impl Cowy) -> Self { let meta = meta.as_ref(); let truncate_pos = meta.char_indices().position(|(i, ch)| { let is_newline = ch == '\n'; diff --git a/src/types/response.rs b/src/types/response.rs index 6a24de7..a76d6a1 100644 --- a/src/types/response.rs +++ b/src/types/response.rs @@ -1,5 +1,6 @@ use anyhow::*; use crate::types::{ResponseHeader, Body, Mime, Document}; +use crate::util::Cowy; use crate::GEMINI_MIME; pub struct Response { @@ -19,12 +20,12 @@ impl Response { Self::success(&GEMINI_MIME).with_body(document) } - pub fn input(prompt: impl AsRef + Into) -> Result { + pub fn input(prompt: impl Cowy) -> Result { let header = ResponseHeader::input(prompt)?; Ok(Self::new(header)) } - pub fn input_lossy(prompt: impl AsRef + Into) -> Self { + pub fn input_lossy(prompt: impl Cowy) -> Self { let header = ResponseHeader::input_lossy(prompt); Self::new(header) } @@ -34,7 +35,7 @@ impl Response { Self::new(header) } - pub fn server_error(reason: impl AsRef + Into) -> Result { + pub fn server_error(reason: impl Cowy) -> Result { let header = ResponseHeader::server_error(reason)?; Ok(Self::new(header)) } diff --git a/src/types/response_header.rs b/src/types/response_header.rs index 8307707..824401e 100644 --- a/src/types/response_header.rs +++ b/src/types/response_header.rs @@ -1,5 +1,6 @@ use anyhow::*; -use mime::Mime; +use crate::Mime; +use crate::util::Cowy; use crate::types::{Status, Meta}; #[derive(Debug,Clone)] @@ -9,14 +10,14 @@ pub struct ResponseHeader { } impl ResponseHeader { - pub fn input(prompt: impl AsRef + Into) -> Result { + pub fn input(prompt: impl Cowy) -> Result { Ok(Self { status: Status::INPUT, meta: Meta::new(prompt).context("Invalid input prompt")?, }) } - pub fn input_lossy(prompt: impl AsRef + Into) -> Self { + pub fn input_lossy(prompt: impl Cowy) -> Self { Self { status: Status::INPUT, meta: Meta::new_lossy(prompt), @@ -30,14 +31,14 @@ impl ResponseHeader { } } - pub fn server_error(reason: impl AsRef + Into) -> Result { + pub fn server_error(reason: impl Cowy) -> Result { Ok(Self { status: Status::PERMANENT_FAILURE, meta: Meta::new(reason).context("Invalid server error reason")?, }) } - pub fn server_error_lossy(reason: impl AsRef + Into) -> Self { + pub fn server_error_lossy(reason: impl Cowy) -> Self { Self { status: Status::PERMANENT_FAILURE, meta: Meta::new_lossy(reason), diff --git a/src/util.rs b/src/util.rs index bd7a28c..4382723 100644 --- a/src/util.rs +++ b/src/util.rs @@ -102,3 +102,19 @@ pub fn guess_mime_from_path>(path: P) -> Mime { mime.parse::().unwrap_or(mime::APPLICATION_OCTET_STREAM) } + +/// A convenience trait alias for `AsRef + Into`, +/// most commonly used to accept `&str` or `String`: +/// +/// `Cowy` ⇔ `AsRef + Into` +pub trait Cowy +where + Self: AsRef + Into, + T: ToOwned + ?Sized, +{} + +impl Cowy for C +where + C: AsRef + Into, + T: ToOwned + ?Sized, +{} From 46077739b3243466cb49683c79253339e2b34fde Mon Sep 17 00:00:00 2001 From: panicbit Date: Sun, 15 Nov 2020 07:01:38 +0100 Subject: [PATCH 04/10] document some Document methods --- src/types/document.rs | 102 +++++++++++++++++++++++++++++++++++++++--- src/util.rs | 2 +- 2 files changed, 97 insertions(+), 7 deletions(-) diff --git a/src/types/document.rs b/src/types/document.rs index 3d65194..8a455fc 100644 --- a/src/types/document.rs +++ b/src/types/document.rs @@ -11,15 +11,61 @@ pub struct Document { } impl Document { + /// Creates an empty Gemini `Document`. + /// + /// # Examples + /// + /// ``` + /// let document = northstar::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 + /// + /// ``` + /// use northstar::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"); + /// ``` pub 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 + /// + /// ``` + /// use northstar::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"); + /// ``` pub fn add_items(&mut self, items: I) -> &mut Self where I: IntoIterator, @@ -28,10 +74,38 @@ impl Document { self } + /// Adds a blank line to the document. + /// + /// # Examples + /// + /// ``` + /// let mut document = northstar::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 = northstar::Document::new(); + /// + /// document.add_text("hello\n* world!"); + /// + /// assert_eq!(document.to_string(), "hello\n * world!\n"); + /// ``` pub fn add_text(&mut self, text: &str) -> &mut Self { let text = text .lines() @@ -43,6 +117,22 @@ impl Document { 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 = northstar::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) -> &mut Self where U: TryInto>, @@ -68,7 +158,7 @@ impl Document { let link = Item::Link(link); self.add_item(link); - + self } @@ -120,7 +210,7 @@ impl Document { .lines() .map(Quote::new_lossy) .map(Item::Quote); - + self.add_items(quote); self @@ -137,7 +227,7 @@ impl fmt::Display for Document { let label = link.label.as_ref().map(|label| label.0.as_str()) .unwrap_or(""); - writeln!(f, "=>{}{}{}", link.uri, separator, label)?; + writeln!(f, "=> {}{}{}", link.uri, separator, label)?; } Item::Preformatted(preformatted) => { writeln!(f, "```{}", preformatted.alt.0)?; @@ -198,7 +288,7 @@ pub struct LinkLabel(String); impl LinkLabel { pub fn from_lossy(line: impl Cowy) -> Self { let line = strip_newlines(line); - + LinkLabel(line) } } @@ -221,7 +311,7 @@ pub struct AltText(String); impl AltText { pub fn new_lossy(alt: &str) -> Self { let alt = strip_newlines(alt); - + Self(alt) } } @@ -261,7 +351,7 @@ pub struct UnorderedListItem(String); impl UnorderedListItem { pub fn new_lossy(text: &str) -> Self { let text = strip_newlines(text); - + Self(text) } } diff --git a/src/util.rs b/src/util.rs index 4382723..8af22d9 100644 --- a/src/util.rs +++ b/src/util.rs @@ -99,7 +99,7 @@ pub fn guess_mime_from_path>(path: P) -> Mime { }, None => "application/octet-stream", }; - + mime.parse::().unwrap_or(mime::APPLICATION_OCTET_STREAM) } From 45c808d0d04c4a613d7b2e4f73f9488de2a8bf31 Mon Sep 17 00:00:00 2001 From: panicbit Date: Sun, 15 Nov 2020 21:14:41 +0100 Subject: [PATCH 05/10] document more Document methods --- src/types/document.rs | 96 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/src/types/document.rs b/src/types/document.rs index 8a455fc..ae6cbb1 100644 --- a/src/types/document.rs +++ b/src/types/document.rs @@ -150,7 +150,27 @@ impl Document { self } - pub fn add_link_without_label(&mut self, uri: URIReference<'static>) -> &mut Self { + /// Adds a link to the document, but without a label. + /// + /// See `add_link` for details. + /// + /// # Examples + /// + /// ``` + /// let mut document = northstar::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>, + { + let uri = uri + .try_into() + .map(URIReference::into_owned) + .or_else(|_| ".".try_into()).expect("Northstar BUG"); let link = Link { uri, label: None, @@ -162,10 +182,40 @@ impl Document { self } + /// Adds a block of preformatted text. + /// + /// Lines that start with ` ``` ` will be prependend with a whitespace. + /// + /// # Examples + /// + /// ``` + /// let mut document = northstar::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: &str) -> &mut Self { self.add_preformatted_with_alt("", preformatted_text) } + /// 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 = northstar::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: &str, preformatted_text: &str) -> &mut Self { let alt = AltText::new_lossy(alt); let lines = preformatted_text @@ -183,6 +233,22 @@ impl Document { self } + /// Adds a heading. + /// + /// Consecutive newlines in `text` will be replaced + /// with a single whitespace. + /// + /// # Examples + /// + /// ``` + /// use northstar::document::HeadingLevel::H1; + /// + /// let mut document = northstar::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) -> &mut Self { let text = HeadingText::new_lossy(text); let heading = Heading { @@ -196,6 +262,21 @@ impl Document { self } + /// Adds an unordered list item. + /// + /// Consecutive newlines in `text` will be replaced + /// with a single whitespace. + /// + /// # Examples + /// + /// ``` + /// let mut document = northstar::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: &str) -> &mut Self { let item = UnorderedListItem::new_lossy(text); let item = Item::UnorderedListItem(item); @@ -205,6 +286,19 @@ impl Document { self } + /// Adds a quote. + /// + /// This function allows adding multiple quote lines at once. + /// + /// # Examples + /// + /// ``` + /// let mut document = northstar::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: &str) -> &mut Self { let quote = text .lines() From ae803a55d22b9ec032a5a38fea9d2ed1fc79aa0a Mon Sep 17 00:00:00 2001 From: panicbit Date: Sun, 15 Nov 2020 21:14:53 +0100 Subject: [PATCH 06/10] do not escape heading text --- src/types/document.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/document.rs b/src/types/document.rs index ae6cbb1..9934c3c 100644 --- a/src/types/document.rs +++ b/src/types/document.rs @@ -436,7 +436,7 @@ impl HeadingText { pub fn new_lossy(line: impl Cowy) -> Self { let line = strip_newlines(line); - Self(lossy_escaped_line(line, &[HEADING_START])) + Self(line) } } From ec6a0af782e3aa15f570071b6ec8bde69824bd93 Mon Sep 17 00:00:00 2001 From: panicbit Date: Sun, 15 Nov 2020 21:17:22 +0100 Subject: [PATCH 07/10] keep document internals private --- src/types/document.rs | 56 +++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/types/document.rs b/src/types/document.rs index 9934c3c..cd0efd8 100644 --- a/src/types/document.rs +++ b/src/types/document.rs @@ -42,7 +42,7 @@ impl Document { /// /// assert_eq!(document.to_string(), "foo\n"); /// ``` - pub fn add_item(&mut self, item: Item) -> &mut Self { + fn add_item(&mut self, item: Item) -> &mut Self { self.items.push(item); self } @@ -66,7 +66,7 @@ impl Document { /// /// assert_eq!(document.to_string(), "foo\nbar\nbaz\n"); /// ``` - pub fn add_items(&mut self, items: I) -> &mut Self + fn add_items(&mut self, items: I) -> &mut Self where I: IntoIterator, { @@ -350,7 +350,7 @@ impl fmt::Display for Document { } } -pub enum Item { +enum Item { Text(Text), Link(Link), Preformatted(Preformatted), @@ -360,59 +360,59 @@ pub enum Item { } #[derive(Default)] -pub struct Text(String); +struct Text(String); impl Text { - pub fn blank() -> Self { + fn blank() -> Self { Self::default() } - pub fn new_lossy(line: impl Cowy) -> Self { + fn new_lossy(line: impl Cowy) -> Self { Self(lossy_escaped_line(line, SPECIAL_STARTS)) } } -pub struct Link { - pub uri: URIReference<'static>, - pub label: Option, +struct Link { + uri: URIReference<'static>, + label: Option, } -pub struct LinkLabel(String); +struct LinkLabel(String); impl LinkLabel { - pub fn from_lossy(line: impl Cowy) -> Self { + fn from_lossy(line: impl Cowy) -> Self { let line = strip_newlines(line); LinkLabel(line) } } -pub struct Preformatted { - pub alt: AltText, - pub lines: Vec, +struct Preformatted { + alt: AltText, + lines: Vec, } -pub struct PreformattedText(String); +struct PreformattedText(String); impl PreformattedText { - pub fn new_lossy(line: impl Cowy) -> Self { + fn new_lossy(line: impl Cowy) -> Self { Self(lossy_escaped_line(line, &[PREFORMATTED_TOGGLE_START])) } } -pub struct AltText(String); +struct AltText(String); impl AltText { - pub fn new_lossy(alt: &str) -> Self { + fn new_lossy(alt: &str) -> Self { let alt = strip_newlines(alt); Self(alt) } } -pub struct Heading { - pub level: HeadingLevel, - pub text: HeadingText, +struct Heading { + level: HeadingLevel, + text: HeadingText, } pub enum HeadingLevel { @@ -422,7 +422,7 @@ pub enum HeadingLevel { } impl Heading { - pub fn new_lossy(level: HeadingLevel, line: &str) -> Self { + fn new_lossy(level: HeadingLevel, line: &str) -> Self { Self { level, text: HeadingText::new_lossy(line), @@ -430,30 +430,30 @@ impl Heading { } } -pub struct HeadingText(String); +struct HeadingText(String); impl HeadingText { - pub fn new_lossy(line: impl Cowy) -> Self { + fn new_lossy(line: impl Cowy) -> Self { let line = strip_newlines(line); Self(line) } } -pub struct UnorderedListItem(String); +struct UnorderedListItem(String); impl UnorderedListItem { - pub fn new_lossy(text: &str) -> Self { + fn new_lossy(text: &str) -> Self { let text = strip_newlines(text); Self(text) } } -pub struct Quote(String); +struct Quote(String); impl Quote { - pub fn new_lossy(text: &str) -> Self { + fn new_lossy(text: &str) -> Self { Self(lossy_escaped_line(text, &[QUOTE_START])) } } From bbeb697bb1f98103c3119ea8595137a1608ea83f Mon Sep 17 00:00:00 2001 From: panicbit Date: Sun, 15 Nov 2020 21:40:16 +0100 Subject: [PATCH 08/10] complete document documentation --- src/types/document.rs | 50 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/src/types/document.rs b/src/types/document.rs index cd0efd8..3780843 100644 --- a/src/types/document.rs +++ b/src/types/document.rs @@ -1,3 +1,41 @@ +//! 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 northstar::document::HeadingLevel::*; +//! +//! let mut document = northstar::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; @@ -6,6 +44,10 @@ use crate::types::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, } @@ -31,7 +73,7 @@ impl Document { /// /// # Examples /// - /// ``` + /// ```compile_fail /// use northstar::document::{Document, Item, Text}; /// /// let mut document = Document::new(); @@ -53,7 +95,7 @@ impl Document { /// /// # Examples /// - /// ``` + /// ```compile_fail /// use northstar::document::{Document, Item, Text}; /// /// let mut document = Document::new(); @@ -415,9 +457,13 @@ struct Heading { text: HeadingText, } +/// The level of a heading. pub enum HeadingLevel { + /// Heading level 1 (`#`) H1, + /// Heading level 2 (`##`) H2, + /// Heading level 3 (`###`) H3, } From add2f30ca63f3847a2f494634976e80f6357768c Mon Sep 17 00:00:00 2001 From: panicbit Date: Sun, 15 Nov 2020 21:42:13 +0100 Subject: [PATCH 09/10] remove unused function --- src/types/document.rs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/types/document.rs b/src/types/document.rs index 3780843..f6fc5aa 100644 --- a/src/types/document.rs +++ b/src/types/document.rs @@ -467,15 +467,6 @@ pub enum HeadingLevel { H3, } -impl Heading { - fn new_lossy(level: HeadingLevel, line: &str) -> Self { - Self { - level, - text: HeadingText::new_lossy(line), - } - } -} - struct HeadingText(String); impl HeadingText { From 09f78666530b371d75aff16bd679f85a124fe8a3 Mon Sep 17 00:00:00 2001 From: panicbit Date: Sun, 15 Nov 2020 21:46:10 +0100 Subject: [PATCH 10/10] update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf5f82e..daade96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- `document` API for creating Gemini documents ## [0.3.0] - 2020-11-14 ### Added