//! Gemtext documents
//!
//! The Gemini specification lays out a lightweight document format to
//! facilitate highly readable and sufficiently interactive content.
//!
//! Presently, this module only provides a way to incrementally build up a
//! gemtext document programmatically, and serialize as utf-8 text using any
//! type that implements `std::fmt::Write`.
//!
//! ```
//! use gemini::Builder;
//!
//! let doc = Builder::new().text("foo").line().quote("bar");
//! assert_eq!(doc.build(), "foo\n\n> bar\n");
//! ```
use std::fmt::Display;
/// Representation of lines in a gemtext document.
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Doc {
/// Text line, corresponding to a paragraph of plain text.
Text(String),
/// Link line, with an optional title.
Link {
/// Link target.
to: String,
/// Optional name for the link.
name: Option<String>,
},
/// A heading to add structure to a document.
Heading(Level, String),
/// An individual list item. Repeated items will form a single list.
ListItem(String),
/// Block quote.
Quote(String),
/// Preformatted text. This is the only document that should contain line breaks.
Preformatted(String),
/// Blank line. Technically equivalent to a Text line, but we use a sentinel to avoid allocation.
Blank,
}
/// Heading level.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Level {
/// H1, top level headings.
One,
/// H2, section headings.
Two,
/// H3, subsection headings.
Three,
}
impl Display for Level {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Level::One => write!(f, "#"),
Level::Two => write!(f, "##"),
Level::Three => write!(f, "###"),
}
}
}
/// A type for incrementally building up a gemtext document that can then be
/// reified into text format.
#[derive(Debug, Default, Clone, Eq, PartialEq)]
pub struct Builder {
docs: Vec<Doc>,
}
impl Builder {
/// Create a new builder, representing the empty document.
pub fn new() -> Self {
Self::default()
}
fn push(mut self, doc: Doc) -> Self {
self.docs.push(doc);
self
}
fn extend(mut self, docs: impl Iterator<Item = Doc>) -> Self {
self.docs.extend(docs);
self
}
/// Add a text line to the document.
pub fn text(self, text: impl Into<String>) -> Self {
self.push(Doc::Text(text.into()))
}
/// Add a blank line to the document.
pub fn line(self) -> Self {
self.push(Doc::Blank)
}
/// Add a link line to the document.
pub fn link(self, to: impl Into<String>, name: Option<impl Into<String>>) -> Self {
let to = to.into();
let name = name.map(Into::into);
self.push(Doc::Link { to, name })
}
/// Add a heading of any level to the document.
pub fn heading(self, level: Level, heading: impl Into<String>) -> Self {
self.push(Doc::Heading(level, heading.into()))
}
/// Add a top level heading to the document.
pub fn h1(self, heading: impl Into<String>) -> Self {
self.heading(Level::One, heading)
}
/// Add a section heading to the document.
pub fn h2(self, heading: impl Into<String>) -> Self {
self.heading(Level::Two, heading)
}
/// Add a subsection heading to the document.
pub fn h3(self, heading: impl Into<String>) -> Self {
self.heading(Level::Three, heading)
}
/// Add an individual list item to the document.
pub fn list_item(self, item: String) -> Self {
self.push(Doc::ListItem(item))
}
/// Add a list to the document, from an iterator of strings.
pub fn list<S: Into<String>>(self, items: impl IntoIterator<Item = S>) -> Self {
let items = items.into_iter().map(|s| Doc::ListItem(s.into()));
self.extend(items)
}
/// Add a block quote to the document.
pub fn quote(self, quote: impl Into<String>) -> Self {
self.push(Doc::Quote(quote.into()))
}
/// Add some preformatted text to the document.
pub fn preformatted(self, preformatted: impl Into<String>) -> Self {
self.push(Doc::Preformatted(preformatted.into()))
}
/// Generate a string representation of a document.
pub fn build(self) -> String {
format!("{}", self)
}
}
impl Display for Builder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.docs.iter().try_for_each(|doc| match doc {
Doc::Text(t) => {
writeln!(f, "{}", t)
}
Doc::Link { to, name } => {
write!(f, "=> {}", to)?;
if let Some(name) = name {
write!(f, " {}", name)?;
}
writeln!(f)
}
Doc::Heading(lvl, h) => {
writeln!(f, "{} {}", lvl, h)
}
Doc::ListItem(i) => {
writeln!(f, "* {}", i)
}
Doc::Quote(q) => {
writeln!(f, "> {}", q)
}
Doc::Preformatted(p) => {
writeln!(f, "```\n{}\n```", p)
}
Doc::Blank => writeln!(f),
})
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
pub fn test_builder() {
let doc = Builder::new()
.preformatted(" wooo\n/^^^^\\\n| |\n\\____/")
.line()
.h1("GAZE INTO THE SPHERE!")
.line()
.text("critics are raving")
.quote("i love the sphere - bort")
.quote("the sphere gives me purpose - frelvin")
.line()
.list(vec!["always", "trust", "the sphere"])
.link("gemini://sphere.gaze", Some("gaze more here"));
assert_eq!(
doc.build(),
r#"```
wooo
/^^^^\
| |
\____/
```
# GAZE INTO THE SPHERE!
critics are raving
> i love the sphere - bort
> the sphere gives me purpose - frelvin
* always
* trust
* the sphere
=> gemini://sphere.gaze gaze more here
"#
)
}
}