use std::{fmt::Display, str::FromStr, string::ToString};
use bumpalo::Bump;
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<'a> {
pub header: Option<Header<'a>>,
pub attributes: DocumentAttributes<'a>,
pub blocks: Vec<Block<'a>>,
pub footnotes: Vec<Footnote<'a>>,
pub toc_entries: Vec<TocEntry<'a>>,
pub location: Location,
}
#[derive(Debug, PartialEq, Serialize)]
#[non_exhaustive]
pub struct Header<'a> {
#[serde(skip_serializing_if = "BlockMetadata::is_default")]
pub metadata: BlockMetadata<'a>,
#[serde(skip_serializing_if = "Title::is_empty")]
pub title: Title<'a>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subtitle: Option<Subtitle<'a>>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub authors: Vec<Author<'a>>,
pub location: Location,
}
#[derive(Debug, PartialEq, Serialize)]
#[non_exhaustive]
pub struct Author<'a> {
#[serde(rename = "firstname")]
pub first_name: &'a str,
#[serde(skip_serializing_if = "Option::is_none", rename = "middlename")]
pub middle_name: Option<&'a str>,
#[serde(rename = "lastname")]
pub last_name: &'a str,
pub initials: &'a str,
#[serde(skip_serializing_if = "Option::is_none", rename = "address")]
pub email: Option<&'a str>,
}
impl<'a> Header<'a> {
#[must_use]
pub fn new(title: Title<'a>, location: Location) -> Self {
Self {
metadata: BlockMetadata::default(),
title,
subtitle: None,
authors: Vec::new(),
location,
}
}
#[must_use]
pub fn with_metadata(mut self, metadata: BlockMetadata<'a>) -> Self {
self.metadata = metadata;
self
}
#[must_use]
pub fn with_subtitle(mut self, subtitle: Subtitle<'a>) -> Self {
self.subtitle = Some(subtitle);
self
}
#[must_use]
pub fn with_authors(mut self, authors: Vec<Author<'a>>) -> Self {
self.authors = authors;
self
}
}
impl<'a> Author<'a> {
#[must_use]
pub fn from_parts(
first_name: &'a str,
middle_name: Option<&'a str>,
last_name: &'a str,
initials: &'a str,
) -> Self {
Self {
first_name,
middle_name,
last_name,
initials,
email: None,
}
}
#[must_use]
pub(crate) fn new(
arena: &'a Bump,
first_name: &'a str,
middle_name: Option<&'a str>,
last_name: Option<&'a str>,
) -> Self {
let first_processed = first_name.replace('_', " ");
let middle_processed = middle_name.map(|m| m.replace('_', " "));
let last_processed = last_name.map(|l| l.replace('_', " "));
let initials = Self::generate_initials(
&first_processed,
middle_processed.as_deref(),
last_processed.as_deref(),
);
Self {
first_name: if first_processed == first_name {
first_name
} else {
arena.alloc_str(&first_processed)
},
middle_name: middle_name
.zip(middle_processed.as_ref())
.map(|(orig, proc)| {
if proc == orig {
orig
} else {
&*arena.alloc_str(proc)
}
}),
last_name: last_name
.zip(last_processed.as_ref())
.map_or("", |(orig, proc)| {
if proc == orig {
orig
} else {
arena.alloc_str(proc)
}
}),
initials: arena.alloc_str(&initials),
email: None,
}
}
#[must_use]
pub fn with_email(mut self, email: &'a str) -> 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<'a> {
pub content: &'a str,
pub location: Location,
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(untagged)]
pub enum Block<'a> {
TableOfContents(TableOfContents<'a>),
Admonition(Admonition<'a>),
DiscreteHeader(DiscreteHeader<'a>),
DocumentAttribute(DocumentAttribute<'a>),
ThematicBreak(ThematicBreak<'a>),
PageBreak(PageBreak<'a>),
UnorderedList(UnorderedList<'a>),
OrderedList(OrderedList<'a>),
CalloutList(CalloutList<'a>),
DescriptionList(DescriptionList<'a>),
Section(Section<'a>),
DelimitedBlock(DelimitedBlock<'a>),
Paragraph(Paragraph<'a>),
Image(Image<'a>),
Audio(Audio<'a>),
Video(Video<'a>),
Comment(Comment<'a>),
}
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<'a> {
pub name: AttributeName<'a>,
pub value: AttributeValue<'a>,
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<'a> {
pub metadata: BlockMetadata<'a>,
pub title: Title<'a>,
pub level: u8,
pub location: Location,
}
#[derive(Clone, Default, Debug, PartialEq)]
#[non_exhaustive]
pub struct ThematicBreak<'a> {
pub anchors: Vec<Anchor<'a>>,
pub title: Title<'a>,
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<'a> {
pub title: Title<'a>,
pub metadata: BlockMetadata<'a>,
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<'a> {
pub metadata: BlockMetadata<'a>,
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<'a> {
pub metadata: BlockMetadata<'a>,
pub title: Title<'a>,
pub content: Vec<InlineNode<'a>>,
pub location: Location,
}
impl<'a> Paragraph<'a> {
#[must_use]
pub fn new(content: Vec<InlineNode<'a>>, location: Location) -> Self {
Self {
metadata: BlockMetadata::default(),
title: Title::default(),
content,
location,
}
}
#[must_use]
pub fn with_metadata(mut self, metadata: BlockMetadata<'a>) -> Self {
self.metadata = metadata;
self
}
#[must_use]
pub fn with_title(mut self, title: Title<'a>) -> Self {
self.title = title;
self
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct DelimitedBlock<'a> {
pub metadata: BlockMetadata<'a>,
pub inner: DelimitedBlockType<'a>,
pub delimiter: &'a str,
pub title: Title<'a>,
pub location: Location,
pub open_delimiter_location: Option<Location>,
pub close_delimiter_location: Option<Location>,
}
impl<'a> DelimitedBlock<'a> {
#[must_use]
pub fn new(inner: DelimitedBlockType<'a>, delimiter: &'a str, 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<'a>) -> Self {
self.metadata = metadata;
self
}
#[must_use]
pub fn with_title(mut self, title: Title<'a>) -> 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<'a> {
pub content: &'a str,
pub notation: StemNotation,
}
impl<'a> StemContent<'a> {
#[must_use]
pub fn new(content: &'a str, notation: StemNotation) -> Self {
Self { content, notation }
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(untagged)]
pub enum DelimitedBlockType<'a> {
DelimitedComment(Vec<InlineNode<'a>>),
DelimitedExample(Vec<Block<'a>>),
DelimitedListing(Vec<InlineNode<'a>>),
DelimitedLiteral(Vec<InlineNode<'a>>),
DelimitedOpen(Vec<Block<'a>>),
DelimitedSidebar(Vec<Block<'a>>),
DelimitedTable(Table<'a>),
DelimitedPass(Vec<InlineNode<'a>>),
DelimitedQuote(Vec<Block<'a>>),
DelimitedVerse(Vec<InlineNode<'a>>),
DelimitedStem(StemContent<'a>),
}
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()
}
}