use std::{fmt::Display, str::FromStr, string::ToString};
use serde::{
Serialize,
ser::{SerializeMap, Serializer},
};
mod admonition;
mod anchor;
mod attributes;
mod attribution;
mod inlines;
mod lists;
mod location;
mod media;
mod metadata;
mod section;
pub(crate) mod substitution;
mod tables;
mod title;
pub use admonition::{Admonition, AdmonitionVariant};
pub use anchor::{Anchor, TocEntry, UNNUMBERED_SECTION_STYLES};
pub use attributes::{
AttributeName, AttributeValue, DocumentAttributes, ElementAttributes, MAX_SECTION_LEVELS,
MAX_TOC_LEVELS, strip_quotes,
};
pub use attribution::{Attribution, CiteTitle};
pub use inlines::*;
pub use lists::{
CalloutList, CalloutListItem, DescriptionList, DescriptionListItem, ListItem,
ListItemCheckedStatus, ListLevel, OrderedList, UnorderedList,
};
pub use location::*;
pub use media::{Audio, Image, Source, SourceUrl, Video};
pub use metadata::{BlockMetadata, Role};
pub use section::*;
pub use substitution::*;
pub use tables::{
ColumnFormat, ColumnStyle, ColumnWidth, HorizontalAlignment, Table, TableColumn, TableRow,
VerticalAlignment,
};
pub use title::{Subtitle, Title};
#[derive(Default, Debug, PartialEq)]
#[non_exhaustive]
pub struct Document {
pub(crate) name: String,
pub(crate) r#type: String,
pub header: Option<Header>,
pub attributes: DocumentAttributes,
pub blocks: Vec<Block>,
pub footnotes: Vec<Footnote>,
pub toc_entries: Vec<TocEntry>,
pub location: Location,
}
#[derive(Debug, PartialEq, Serialize)]
#[non_exhaustive]
pub struct Header {
#[serde(skip_serializing_if = "BlockMetadata::is_default")]
pub metadata: BlockMetadata,
#[serde(skip_serializing_if = "Title::is_empty")]
pub title: Title,
#[serde(skip_serializing_if = "Option::is_none")]
pub subtitle: Option<Subtitle>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub authors: Vec<Author>,
pub location: Location,
}
#[derive(Debug, PartialEq, Serialize)]
#[non_exhaustive]
pub struct Author {
#[serde(rename = "firstname")]
pub first_name: String,
#[serde(skip_serializing_if = "Option::is_none", rename = "middlename")]
pub middle_name: Option<String>,
#[serde(rename = "lastname")]
pub last_name: String,
pub initials: String,
#[serde(skip_serializing_if = "Option::is_none", rename = "address")]
pub email: Option<String>,
}
impl Header {
#[must_use]
pub fn new(title: Title, location: Location) -> Self {
Self {
metadata: BlockMetadata::default(),
title,
subtitle: None,
authors: Vec::new(),
location,
}
}
#[must_use]
pub fn with_metadata(mut self, metadata: BlockMetadata) -> Self {
self.metadata = metadata;
self
}
#[must_use]
pub fn with_subtitle(mut self, subtitle: Subtitle) -> Self {
self.subtitle = Some(subtitle);
self
}
#[must_use]
pub fn with_authors(mut self, authors: Vec<Author>) -> Self {
self.authors = authors;
self
}
}
impl Author {
#[must_use]
pub fn new(first_name: &str, middle_name: Option<&str>, last_name: Option<&str>) -> Self {
let first_name = first_name.replace('_', " ");
let middle_name = middle_name.map(|m| m.replace('_', " "));
let last_name = last_name.map(|l| l.replace('_', " ")).unwrap_or_default();
let initials =
Self::generate_initials(&first_name, middle_name.as_deref(), Some(&last_name));
Self {
first_name,
middle_name,
last_name,
initials,
email: None,
}
}
#[must_use]
pub fn with_email(mut self, email: String) -> Self {
self.email = Some(email);
self
}
fn generate_initials(first: &str, middle: Option<&str>, last: Option<&str>) -> String {
let first_initial = first.chars().next().unwrap_or_default().to_string();
let middle_initial = middle
.map(|m| m.chars().next().unwrap_or_default().to_string())
.unwrap_or_default();
let last_initial = last
.map(|m| m.chars().next().unwrap_or_default().to_string())
.unwrap_or_default();
first_initial + &middle_initial + &last_initial
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct Comment {
pub content: String,
pub location: Location,
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(untagged)]
pub enum Block {
TableOfContents(TableOfContents),
Admonition(Admonition),
DiscreteHeader(DiscreteHeader),
DocumentAttribute(DocumentAttribute),
ThematicBreak(ThematicBreak),
PageBreak(PageBreak),
UnorderedList(UnorderedList),
OrderedList(OrderedList),
CalloutList(CalloutList),
DescriptionList(DescriptionList),
Section(Section),
DelimitedBlock(DelimitedBlock),
Paragraph(Paragraph),
Image(Image),
Audio(Audio),
Video(Video),
Comment(Comment),
}
impl Locateable for Block {
fn location(&self) -> &Location {
match self {
Block::Section(s) => &s.location,
Block::Paragraph(p) => &p.location,
Block::UnorderedList(l) => &l.location,
Block::OrderedList(l) => &l.location,
Block::DescriptionList(l) => &l.location,
Block::CalloutList(l) => &l.location,
Block::DelimitedBlock(d) => &d.location,
Block::Admonition(a) => &a.location,
Block::TableOfContents(t) => &t.location,
Block::DiscreteHeader(h) => &h.location,
Block::DocumentAttribute(a) => &a.location,
Block::ThematicBreak(tb) => &tb.location,
Block::PageBreak(pb) => &pb.location,
Block::Image(i) => &i.location,
Block::Audio(a) => &a.location,
Block::Video(v) => &v.location,
Block::Comment(c) => &c.location,
}
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct DocumentAttribute {
pub name: AttributeName,
pub value: AttributeValue,
pub location: Location,
}
impl Serialize for DocumentAttribute {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_map(None)?;
state.serialize_entry("name", &self.name)?;
state.serialize_entry("type", "attribute")?;
state.serialize_entry("value", &self.value)?;
state.serialize_entry("location", &self.location)?;
state.end()
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct DiscreteHeader {
pub metadata: BlockMetadata,
pub title: Title,
pub level: u8,
pub location: Location,
}
#[derive(Clone, Default, Debug, PartialEq)]
#[non_exhaustive]
pub struct ThematicBreak {
pub anchors: Vec<Anchor>,
pub title: Title,
pub location: Location,
}
impl Serialize for ThematicBreak {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_map(None)?;
state.serialize_entry("name", "break")?;
state.serialize_entry("type", "block")?;
state.serialize_entry("variant", "thematic")?;
if !self.anchors.is_empty() {
state.serialize_entry("anchors", &self.anchors)?;
}
if !self.title.is_empty() {
state.serialize_entry("title", &self.title)?;
}
state.serialize_entry("location", &self.location)?;
state.end()
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct PageBreak {
pub title: Title,
pub metadata: BlockMetadata,
pub location: Location,
}
impl Serialize for PageBreak {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_map(None)?;
state.serialize_entry("name", "break")?;
state.serialize_entry("type", "block")?;
state.serialize_entry("variant", "page")?;
if !self.title.is_empty() {
state.serialize_entry("title", &self.title)?;
}
if !self.metadata.is_default() {
state.serialize_entry("metadata", &self.metadata)?;
}
state.serialize_entry("location", &self.location)?;
state.end()
}
}
impl Serialize for Comment {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_map(None)?;
state.serialize_entry("name", "comment")?;
state.serialize_entry("type", "block")?;
if !self.content.is_empty() {
state.serialize_entry("content", &self.content)?;
}
state.serialize_entry("location", &self.location)?;
state.end()
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct TableOfContents {
pub metadata: BlockMetadata,
pub location: Location,
}
impl Serialize for TableOfContents {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_map(None)?;
state.serialize_entry("name", "toc")?;
state.serialize_entry("type", "block")?;
if !self.metadata.is_default() {
state.serialize_entry("metadata", &self.metadata)?;
}
state.serialize_entry("location", &self.location)?;
state.end()
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct Paragraph {
pub metadata: BlockMetadata,
pub title: Title,
pub content: Vec<InlineNode>,
pub location: Location,
}
impl Paragraph {
#[must_use]
pub fn new(content: Vec<InlineNode>, location: Location) -> Self {
Self {
metadata: BlockMetadata::default(),
title: Title::default(),
content,
location,
}
}
#[must_use]
pub fn with_metadata(mut self, metadata: BlockMetadata) -> Self {
self.metadata = metadata;
self
}
#[must_use]
pub fn with_title(mut self, title: Title) -> Self {
self.title = title;
self
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct DelimitedBlock {
pub metadata: BlockMetadata,
pub inner: DelimitedBlockType,
pub delimiter: String,
pub title: Title,
pub location: Location,
pub open_delimiter_location: Option<Location>,
pub close_delimiter_location: Option<Location>,
}
impl DelimitedBlock {
#[must_use]
pub fn new(inner: DelimitedBlockType, delimiter: String, location: Location) -> Self {
Self {
metadata: BlockMetadata::default(),
inner,
delimiter,
title: Title::default(),
location,
open_delimiter_location: None,
close_delimiter_location: None,
}
}
#[must_use]
pub fn with_metadata(mut self, metadata: BlockMetadata) -> Self {
self.metadata = metadata;
self
}
#[must_use]
pub fn with_title(mut self, title: Title) -> Self {
self.title = title;
self
}
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum StemNotation {
Latexmath,
Asciimath,
}
impl Display for StemNotation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
StemNotation::Latexmath => write!(f, "latexmath"),
StemNotation::Asciimath => write!(f, "asciimath"),
}
}
}
impl FromStr for StemNotation {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"latexmath" => Ok(Self::Latexmath),
"asciimath" => Ok(Self::Asciimath),
_ => Err(format!("unknown stem notation: {s}")),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[non_exhaustive]
pub struct StemContent {
pub content: String,
pub notation: StemNotation,
}
impl StemContent {
#[must_use]
pub fn new(content: String, notation: StemNotation) -> Self {
Self { content, notation }
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(untagged)]
pub enum DelimitedBlockType {
DelimitedComment(Vec<InlineNode>),
DelimitedExample(Vec<Block>),
DelimitedListing(Vec<InlineNode>),
DelimitedLiteral(Vec<InlineNode>),
DelimitedOpen(Vec<Block>),
DelimitedSidebar(Vec<Block>),
DelimitedTable(Table),
DelimitedPass(Vec<InlineNode>),
DelimitedQuote(Vec<Block>),
DelimitedVerse(Vec<InlineNode>),
DelimitedStem(StemContent),
}
impl DelimitedBlockType {
fn name(&self) -> &'static str {
match self {
DelimitedBlockType::DelimitedComment(_) => "comment",
DelimitedBlockType::DelimitedExample(_) => "example",
DelimitedBlockType::DelimitedListing(_) => "listing",
DelimitedBlockType::DelimitedLiteral(_) => "literal",
DelimitedBlockType::DelimitedOpen(_) => "open",
DelimitedBlockType::DelimitedSidebar(_) => "sidebar",
DelimitedBlockType::DelimitedTable(_) => "table",
DelimitedBlockType::DelimitedPass(_) => "pass",
DelimitedBlockType::DelimitedQuote(_) => "quote",
DelimitedBlockType::DelimitedVerse(_) => "verse",
DelimitedBlockType::DelimitedStem(_) => "stem",
}
}
}
impl Serialize for Document {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_map(None)?;
state.serialize_entry("name", "document")?;
state.serialize_entry("type", "block")?;
if let Some(header) = &self.header {
state.serialize_entry("header", header)?;
state.serialize_entry("attributes", &self.attributes)?;
} else if !self.attributes.is_empty() {
state.serialize_entry("attributes", &self.attributes)?;
}
if !self.blocks.is_empty() {
state.serialize_entry("blocks", &self.blocks)?;
}
state.serialize_entry("location", &self.location)?;
state.end()
}
}
impl Serialize for DelimitedBlock {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_map(None)?;
state.serialize_entry("name", self.inner.name())?;
state.serialize_entry("type", "block")?;
state.serialize_entry("form", "delimited")?;
state.serialize_entry("delimiter", &self.delimiter)?;
if !self.metadata.is_default() {
state.serialize_entry("metadata", &self.metadata)?;
}
match &self.inner {
DelimitedBlockType::DelimitedStem(stem) => {
state.serialize_entry("content", &stem.content)?;
state.serialize_entry("notation", &stem.notation)?;
}
DelimitedBlockType::DelimitedListing(inner)
| DelimitedBlockType::DelimitedLiteral(inner)
| DelimitedBlockType::DelimitedPass(inner)
| DelimitedBlockType::DelimitedVerse(inner) => {
state.serialize_entry("inlines", &inner)?;
}
DelimitedBlockType::DelimitedTable(inner) => {
state.serialize_entry("content", &inner)?;
}
inner @ (DelimitedBlockType::DelimitedComment(_)
| DelimitedBlockType::DelimitedExample(_)
| DelimitedBlockType::DelimitedOpen(_)
| DelimitedBlockType::DelimitedQuote(_)
| DelimitedBlockType::DelimitedSidebar(_)) => {
state.serialize_entry("blocks", &inner)?;
}
}
if !self.title.is_empty() {
state.serialize_entry("title", &self.title)?;
}
state.serialize_entry("location", &self.location)?;
state.end()
}
}
impl Serialize for DiscreteHeader {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_map(None)?;
state.serialize_entry("name", "heading")?;
state.serialize_entry("type", "block")?;
if !self.title.is_empty() {
state.serialize_entry("title", &self.title)?;
}
state.serialize_entry("level", &self.level)?;
if !self.metadata.is_default() {
state.serialize_entry("metadata", &self.metadata)?;
}
state.serialize_entry("location", &self.location)?;
state.end()
}
}
impl Serialize for Paragraph {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_map(None)?;
state.serialize_entry("name", "paragraph")?;
state.serialize_entry("type", "block")?;
if !self.title.is_empty() {
state.serialize_entry("title", &self.title)?;
}
state.serialize_entry("inlines", &self.content)?;
if !self.metadata.is_default() {
state.serialize_entry("metadata", &self.metadata)?;
}
state.serialize_entry("location", &self.location)?;
state.end()
}
}