1use std::{fmt::Display, str::FromStr, string::ToString};
3
4use bumpalo::Bump;
5use serde::{
6 Serialize,
7 ser::{SerializeMap, Serializer},
8};
9
10mod admonition;
11mod anchor;
12mod attributes;
13mod attribution;
14mod inlines;
15mod lists;
16mod location;
17mod media;
18mod metadata;
19mod section;
20pub(crate) mod substitution;
21mod tables;
22mod title;
23
24pub use admonition::{Admonition, AdmonitionVariant};
25pub use anchor::{Anchor, TocEntry, UNNUMBERED_SECTION_STYLES};
26pub use attributes::{
27 AttributeName, AttributeValue, DocumentAttributes, ElementAttributes, MAX_SECTION_LEVELS,
28 MAX_TOC_LEVELS, strip_quotes,
29};
30pub use attribution::{Attribution, CiteTitle};
31pub use inlines::*;
32pub use lists::{
33 CalloutList, CalloutListItem, DescriptionList, DescriptionListItem, ListItem,
34 ListItemCheckedStatus, ListLevel, OrderedList, UnorderedList,
35};
36pub use location::*;
37pub use media::{Audio, Image, Source, SourceUrl, Video};
38pub use metadata::{BlockMetadata, Role};
39pub use section::*;
40pub use substitution::*;
41pub use tables::{
42 ColumnFormat, ColumnStyle, ColumnWidth, HorizontalAlignment, Table, TableColumn, TableRow,
43 VerticalAlignment,
44};
45pub use title::{Subtitle, Title};
46
47#[derive(Default, Debug, PartialEq)]
49#[non_exhaustive]
50pub struct Document<'a> {
51 pub header: Option<Header<'a>>,
52 pub attributes: DocumentAttributes<'a>,
53 pub blocks: Vec<Block<'a>>,
54 pub footnotes: Vec<Footnote<'a>>,
55 pub toc_entries: Vec<TocEntry<'a>>,
56 pub location: Location,
57}
58
59#[derive(Debug, PartialEq, Serialize)]
64#[non_exhaustive]
65pub struct Header<'a> {
66 #[serde(skip_serializing_if = "BlockMetadata::is_default")]
67 pub metadata: BlockMetadata<'a>,
68 #[serde(skip_serializing_if = "Title::is_empty")]
69 pub title: Title<'a>,
70 #[serde(skip_serializing_if = "Option::is_none")]
71 pub subtitle: Option<Subtitle<'a>>,
72 #[serde(skip_serializing_if = "Vec::is_empty")]
73 pub authors: Vec<Author<'a>>,
74 pub location: Location,
75}
76
77#[derive(Debug, PartialEq, Serialize)]
79#[non_exhaustive]
80pub struct Author<'a> {
81 #[serde(rename = "firstname")]
82 pub first_name: &'a str,
83 #[serde(skip_serializing_if = "Option::is_none", rename = "middlename")]
84 pub middle_name: Option<&'a str>,
85 #[serde(rename = "lastname")]
86 pub last_name: &'a str,
87 pub initials: &'a str,
88 #[serde(skip_serializing_if = "Option::is_none", rename = "address")]
89 pub email: Option<&'a str>,
90}
91
92impl<'a> Header<'a> {
93 #[must_use]
95 pub fn new(title: Title<'a>, location: Location) -> Self {
96 Self {
97 metadata: BlockMetadata::default(),
98 title,
99 subtitle: None,
100 authors: Vec::new(),
101 location,
102 }
103 }
104
105 #[must_use]
107 pub fn with_metadata(mut self, metadata: BlockMetadata<'a>) -> Self {
108 self.metadata = metadata;
109 self
110 }
111
112 #[must_use]
114 pub fn with_subtitle(mut self, subtitle: Subtitle<'a>) -> Self {
115 self.subtitle = Some(subtitle);
116 self
117 }
118
119 #[must_use]
121 pub fn with_authors(mut self, authors: Vec<Author<'a>>) -> Self {
122 self.authors = authors;
123 self
124 }
125}
126
127impl<'a> Author<'a> {
128 #[must_use]
132 pub fn from_parts(
133 first_name: &'a str,
134 middle_name: Option<&'a str>,
135 last_name: &'a str,
136 initials: &'a str,
137 ) -> Self {
138 Self {
139 first_name,
140 middle_name,
141 last_name,
142 initials,
143 email: None,
144 }
145 }
146
147 #[must_use]
150 pub(crate) fn new(
151 arena: &'a Bump,
152 first_name: &'a str,
153 middle_name: Option<&'a str>,
154 last_name: Option<&'a str>,
155 ) -> Self {
156 let first_processed = first_name.replace('_', " ");
157 let middle_processed = middle_name.map(|m| m.replace('_', " "));
158 let last_processed = last_name.map(|l| l.replace('_', " "));
159
160 let initials = Self::generate_initials(
161 &first_processed,
162 middle_processed.as_deref(),
163 last_processed.as_deref(),
164 );
165
166 Self {
167 first_name: if first_processed == first_name {
168 first_name
169 } else {
170 arena.alloc_str(&first_processed)
171 },
172 middle_name: middle_name
173 .zip(middle_processed.as_ref())
174 .map(|(orig, proc)| {
175 if proc == orig {
176 orig
177 } else {
178 &*arena.alloc_str(proc)
179 }
180 }),
181 last_name: last_name
182 .zip(last_processed.as_ref())
183 .map_or("", |(orig, proc)| {
184 if proc == orig {
185 orig
186 } else {
187 arena.alloc_str(proc)
188 }
189 }),
190 initials: arena.alloc_str(&initials),
191 email: None,
192 }
193 }
194
195 #[must_use]
197 pub fn with_email(mut self, email: &'a str) -> Self {
198 self.email = Some(email);
199 self
200 }
201
202 fn generate_initials(first: &str, middle: Option<&str>, last: Option<&str>) -> String {
204 let first_initial = first.chars().next().unwrap_or_default().to_string();
205 let middle_initial = middle
206 .map(|m| m.chars().next().unwrap_or_default().to_string())
207 .unwrap_or_default();
208 let last_initial = last
209 .map(|m| m.chars().next().unwrap_or_default().to_string())
210 .unwrap_or_default();
211 first_initial + &middle_initial + &last_initial
212 }
213}
214
215#[derive(Clone, Debug, PartialEq)]
220#[non_exhaustive]
221pub struct Comment<'a> {
222 pub content: &'a str,
223 pub location: Location,
224}
225
226#[non_exhaustive]
230#[derive(Clone, Debug, PartialEq, Serialize)]
231#[serde(untagged)]
232pub enum Block<'a> {
233 TableOfContents(TableOfContents<'a>),
234 Admonition(Admonition<'a>),
240 DiscreteHeader(DiscreteHeader<'a>),
241 DocumentAttribute(DocumentAttribute<'a>),
242 ThematicBreak(ThematicBreak<'a>),
243 PageBreak(PageBreak<'a>),
244 UnorderedList(UnorderedList<'a>),
245 OrderedList(OrderedList<'a>),
246 CalloutList(CalloutList<'a>),
247 DescriptionList(DescriptionList<'a>),
248 Section(Section<'a>),
249 DelimitedBlock(DelimitedBlock<'a>),
250 Paragraph(Paragraph<'a>),
251 Image(Image<'a>),
252 Audio(Audio<'a>),
253 Video(Video<'a>),
254 Comment(Comment<'a>),
255}
256
257impl Locateable for Block<'_> {
258 fn location(&self) -> &Location {
259 match self {
260 Block::Section(s) => &s.location,
261 Block::Paragraph(p) => &p.location,
262 Block::UnorderedList(l) => &l.location,
263 Block::OrderedList(l) => &l.location,
264 Block::DescriptionList(l) => &l.location,
265 Block::CalloutList(l) => &l.location,
266 Block::DelimitedBlock(d) => &d.location,
267 Block::Admonition(a) => &a.location,
268 Block::TableOfContents(t) => &t.location,
269 Block::DiscreteHeader(h) => &h.location,
270 Block::DocumentAttribute(a) => &a.location,
271 Block::ThematicBreak(tb) => &tb.location,
272 Block::PageBreak(pb) => &pb.location,
273 Block::Image(i) => &i.location,
274 Block::Audio(a) => &a.location,
275 Block::Video(v) => &v.location,
276 Block::Comment(c) => &c.location,
277 }
278 }
279}
280
281#[derive(Clone, Debug, PartialEq)]
286#[non_exhaustive]
287pub struct DocumentAttribute<'a> {
288 pub name: AttributeName<'a>,
289 pub value: AttributeValue<'a>,
290 pub location: Location,
291}
292
293impl Serialize for DocumentAttribute<'_> {
294 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
295 where
296 S: Serializer,
297 {
298 let mut state = serializer.serialize_map(None)?;
299 state.serialize_entry("name", &self.name)?;
300 state.serialize_entry("type", "attribute")?;
301 state.serialize_entry("value", &self.value)?;
302 state.serialize_entry("location", &self.location)?;
303 state.end()
304 }
305}
306
307#[derive(Clone, Debug, PartialEq)]
312#[non_exhaustive]
313pub struct DiscreteHeader<'a> {
314 pub metadata: BlockMetadata<'a>,
315 pub title: Title<'a>,
316 pub level: u8,
317 pub location: Location,
318}
319
320#[derive(Clone, Default, Debug, PartialEq)]
322#[non_exhaustive]
323pub struct ThematicBreak<'a> {
324 pub anchors: Vec<Anchor<'a>>,
325 pub title: Title<'a>,
326 pub location: Location,
327}
328
329impl Serialize for ThematicBreak<'_> {
330 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
331 where
332 S: Serializer,
333 {
334 let mut state = serializer.serialize_map(None)?;
335 state.serialize_entry("name", "break")?;
336 state.serialize_entry("type", "block")?;
337 state.serialize_entry("variant", "thematic")?;
338 if !self.anchors.is_empty() {
339 state.serialize_entry("anchors", &self.anchors)?;
340 }
341 if !self.title.is_empty() {
342 state.serialize_entry("title", &self.title)?;
343 }
344 state.serialize_entry("location", &self.location)?;
345 state.end()
346 }
347}
348
349#[derive(Clone, Debug, PartialEq)]
351#[non_exhaustive]
352pub struct PageBreak<'a> {
353 pub title: Title<'a>,
354 pub metadata: BlockMetadata<'a>,
355 pub location: Location,
356}
357
358impl Serialize for PageBreak<'_> {
359 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
360 where
361 S: Serializer,
362 {
363 let mut state = serializer.serialize_map(None)?;
364 state.serialize_entry("name", "break")?;
365 state.serialize_entry("type", "block")?;
366 state.serialize_entry("variant", "page")?;
367 if !self.title.is_empty() {
368 state.serialize_entry("title", &self.title)?;
369 }
370 if !self.metadata.is_default() {
371 state.serialize_entry("metadata", &self.metadata)?;
372 }
373 state.serialize_entry("location", &self.location)?;
374 state.end()
375 }
376}
377
378impl Serialize for Comment<'_> {
379 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
380 where
381 S: Serializer,
382 {
383 let mut state = serializer.serialize_map(None)?;
384 state.serialize_entry("name", "comment")?;
385 state.serialize_entry("type", "block")?;
386 if !self.content.is_empty() {
387 state.serialize_entry("content", &self.content)?;
388 }
389 state.serialize_entry("location", &self.location)?;
390 state.end()
391 }
392}
393
394#[derive(Clone, Debug, PartialEq)]
396#[non_exhaustive]
397pub struct TableOfContents<'a> {
398 pub metadata: BlockMetadata<'a>,
399 pub location: Location,
400}
401
402impl Serialize for TableOfContents<'_> {
403 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
404 where
405 S: Serializer,
406 {
407 let mut state = serializer.serialize_map(None)?;
408 state.serialize_entry("name", "toc")?;
409 state.serialize_entry("type", "block")?;
410 if !self.metadata.is_default() {
411 state.serialize_entry("metadata", &self.metadata)?;
412 }
413 state.serialize_entry("location", &self.location)?;
414 state.end()
415 }
416}
417
418#[derive(Clone, Debug, PartialEq)]
420#[non_exhaustive]
421pub struct Paragraph<'a> {
422 pub metadata: BlockMetadata<'a>,
423 pub title: Title<'a>,
424 pub content: Vec<InlineNode<'a>>,
425 pub location: Location,
426}
427
428impl<'a> Paragraph<'a> {
429 #[must_use]
431 pub fn new(content: Vec<InlineNode<'a>>, location: Location) -> Self {
432 Self {
433 metadata: BlockMetadata::default(),
434 title: Title::default(),
435 content,
436 location,
437 }
438 }
439
440 #[must_use]
442 pub fn with_metadata(mut self, metadata: BlockMetadata<'a>) -> Self {
443 self.metadata = metadata;
444 self
445 }
446
447 #[must_use]
449 pub fn with_title(mut self, title: Title<'a>) -> Self {
450 self.title = title;
451 self
452 }
453}
454
455#[derive(Clone, Debug, PartialEq)]
457#[non_exhaustive]
458pub struct DelimitedBlock<'a> {
459 pub metadata: BlockMetadata<'a>,
460 pub inner: DelimitedBlockType<'a>,
461 pub delimiter: &'a str,
462 pub title: Title<'a>,
463 pub location: Location,
464 pub open_delimiter_location: Option<Location>,
465 pub close_delimiter_location: Option<Location>,
466}
467
468impl<'a> DelimitedBlock<'a> {
469 #[must_use]
471 pub fn new(inner: DelimitedBlockType<'a>, delimiter: &'a str, location: Location) -> Self {
472 Self {
473 metadata: BlockMetadata::default(),
474 inner,
475 delimiter,
476 title: Title::default(),
477 location,
478 open_delimiter_location: None,
479 close_delimiter_location: None,
480 }
481 }
482
483 #[must_use]
485 pub fn with_metadata(mut self, metadata: BlockMetadata<'a>) -> Self {
486 self.metadata = metadata;
487 self
488 }
489
490 #[must_use]
492 pub fn with_title(mut self, title: Title<'a>) -> Self {
493 self.title = title;
494 self
495 }
496}
497
498#[derive(Clone, Debug, PartialEq, Serialize)]
500#[serde(rename_all = "lowercase")]
501pub enum StemNotation {
502 Latexmath,
503 Asciimath,
504}
505
506impl Display for StemNotation {
507 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
508 match self {
509 StemNotation::Latexmath => write!(f, "latexmath"),
510 StemNotation::Asciimath => write!(f, "asciimath"),
511 }
512 }
513}
514
515impl FromStr for StemNotation {
516 type Err = String;
517
518 fn from_str(s: &str) -> Result<Self, Self::Err> {
519 match s {
520 "latexmath" => Ok(Self::Latexmath),
521 "asciimath" => Ok(Self::Asciimath),
522 _ => Err(format!("unknown stem notation: {s}")),
523 }
524 }
525}
526
527#[derive(Clone, Debug, PartialEq, Serialize)]
529#[non_exhaustive]
530pub struct StemContent<'a> {
531 pub content: &'a str,
532 pub notation: StemNotation,
533}
534
535impl<'a> StemContent<'a> {
536 #[must_use]
538 pub fn new(content: &'a str, notation: StemNotation) -> Self {
539 Self { content, notation }
540 }
541}
542
543#[non_exhaustive]
580#[derive(Clone, Debug, PartialEq, Serialize)]
581#[serde(untagged)]
582pub enum DelimitedBlockType<'a> {
583 DelimitedComment(Vec<InlineNode<'a>>),
585 DelimitedExample(Vec<Block<'a>>),
587 DelimitedListing(Vec<InlineNode<'a>>),
589 DelimitedLiteral(Vec<InlineNode<'a>>),
591 DelimitedOpen(Vec<Block<'a>>),
593 DelimitedSidebar(Vec<Block<'a>>),
595 DelimitedTable(Table<'a>),
597 DelimitedPass(Vec<InlineNode<'a>>),
599 DelimitedQuote(Vec<Block<'a>>),
601 DelimitedVerse(Vec<InlineNode<'a>>),
603 DelimitedStem(StemContent<'a>),
605}
606
607impl DelimitedBlockType<'_> {
608 fn name(&self) -> &'static str {
609 match self {
610 DelimitedBlockType::DelimitedComment(_) => "comment",
611 DelimitedBlockType::DelimitedExample(_) => "example",
612 DelimitedBlockType::DelimitedListing(_) => "listing",
613 DelimitedBlockType::DelimitedLiteral(_) => "literal",
614 DelimitedBlockType::DelimitedOpen(_) => "open",
615 DelimitedBlockType::DelimitedSidebar(_) => "sidebar",
616 DelimitedBlockType::DelimitedTable(_) => "table",
617 DelimitedBlockType::DelimitedPass(_) => "pass",
618 DelimitedBlockType::DelimitedQuote(_) => "quote",
619 DelimitedBlockType::DelimitedVerse(_) => "verse",
620 DelimitedBlockType::DelimitedStem(_) => "stem",
621 }
622 }
623}
624
625impl Serialize for Document<'_> {
626 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
627 where
628 S: Serializer,
629 {
630 let mut state = serializer.serialize_map(None)?;
631 state.serialize_entry("name", "document")?;
632 state.serialize_entry("type", "block")?;
633 if let Some(header) = &self.header {
634 state.serialize_entry("header", header)?;
635 state.serialize_entry("attributes", &self.attributes)?;
638 } else if !self.attributes.is_empty() {
639 state.serialize_entry("attributes", &self.attributes)?;
640 }
641 if !self.blocks.is_empty() {
642 state.serialize_entry("blocks", &self.blocks)?;
643 }
644 state.serialize_entry("location", &self.location)?;
645 state.end()
646 }
647}
648
649impl Serialize for DelimitedBlock<'_> {
650 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
651 where
652 S: Serializer,
653 {
654 let mut state = serializer.serialize_map(None)?;
655 state.serialize_entry("name", self.inner.name())?;
656 state.serialize_entry("type", "block")?;
657 state.serialize_entry("form", "delimited")?;
658 state.serialize_entry("delimiter", &self.delimiter)?;
659 if !self.metadata.is_default() {
660 state.serialize_entry("metadata", &self.metadata)?;
661 }
662
663 match &self.inner {
664 DelimitedBlockType::DelimitedStem(stem) => {
665 state.serialize_entry("content", &stem.content)?;
666 state.serialize_entry("notation", &stem.notation)?;
667 }
668 DelimitedBlockType::DelimitedListing(inner)
669 | DelimitedBlockType::DelimitedLiteral(inner)
670 | DelimitedBlockType::DelimitedPass(inner)
671 | DelimitedBlockType::DelimitedVerse(inner) => {
672 state.serialize_entry("inlines", &inner)?;
673 }
674 DelimitedBlockType::DelimitedTable(inner) => {
675 state.serialize_entry("content", &inner)?;
676 }
677 inner @ (DelimitedBlockType::DelimitedComment(_)
678 | DelimitedBlockType::DelimitedExample(_)
679 | DelimitedBlockType::DelimitedOpen(_)
680 | DelimitedBlockType::DelimitedQuote(_)
681 | DelimitedBlockType::DelimitedSidebar(_)) => {
682 state.serialize_entry("blocks", &inner)?;
683 }
684 }
685 if !self.title.is_empty() {
686 state.serialize_entry("title", &self.title)?;
687 }
688 state.serialize_entry("location", &self.location)?;
689 state.end()
690 }
691}
692
693impl Serialize for DiscreteHeader<'_> {
694 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
695 where
696 S: Serializer,
697 {
698 let mut state = serializer.serialize_map(None)?;
699 state.serialize_entry("name", "heading")?;
700 state.serialize_entry("type", "block")?;
701 if !self.title.is_empty() {
702 state.serialize_entry("title", &self.title)?;
703 }
704 state.serialize_entry("level", &self.level)?;
705 if !self.metadata.is_default() {
706 state.serialize_entry("metadata", &self.metadata)?;
707 }
708 state.serialize_entry("location", &self.location)?;
709 state.end()
710 }
711}
712
713impl Serialize for Paragraph<'_> {
714 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
715 where
716 S: Serializer,
717 {
718 let mut state = serializer.serialize_map(None)?;
719 state.serialize_entry("name", "paragraph")?;
720 state.serialize_entry("type", "block")?;
721 if !self.title.is_empty() {
722 state.serialize_entry("title", &self.title)?;
723 }
724 state.serialize_entry("inlines", &self.content)?;
725 if !self.metadata.is_default() {
726 state.serialize_entry("metadata", &self.metadata)?;
727 }
728 state.serialize_entry("location", &self.location)?;
729 state.end()
730 }
731}