1use std::{fmt::Display, str::FromStr, string::ToString};
3
4use serde::{
5 Serialize,
6 ser::{SerializeMap, Serializer},
7};
8
9mod admonition;
10mod anchor;
11mod attributes;
12mod attribution;
13mod inlines;
14mod lists;
15mod location;
16mod media;
17mod metadata;
18mod section;
19pub(crate) mod substitution;
20mod tables;
21mod title;
22
23pub use admonition::{Admonition, AdmonitionVariant};
24pub use anchor::{Anchor, TocEntry, UNNUMBERED_SECTION_STYLES};
25pub use attributes::{
26 AttributeName, AttributeValue, DocumentAttributes, ElementAttributes, MAX_SECTION_LEVELS,
27 MAX_TOC_LEVELS, strip_quotes,
28};
29pub use attribution::{Attribution, CiteTitle};
30pub use inlines::*;
31pub use lists::{
32 CalloutList, CalloutListItem, DescriptionList, DescriptionListItem, ListItem,
33 ListItemCheckedStatus, ListLevel, OrderedList, UnorderedList,
34};
35pub use location::*;
36pub use media::{Audio, Image, Source, SourceUrl, Video};
37pub use metadata::{BlockMetadata, Role};
38pub use section::*;
39pub use substitution::*;
40pub use tables::{
41 ColumnFormat, ColumnStyle, ColumnWidth, HorizontalAlignment, Table, TableColumn, TableRow,
42 VerticalAlignment,
43};
44pub use title::{Subtitle, Title};
45
46#[derive(Default, Debug, PartialEq)]
48#[non_exhaustive]
49pub struct Document {
50 pub(crate) name: String,
51 pub(crate) r#type: String,
52 pub header: Option<Header>,
53 pub attributes: DocumentAttributes,
54 pub blocks: Vec<Block>,
55 pub footnotes: Vec<Footnote>,
56 pub toc_entries: Vec<TocEntry>,
57 pub location: Location,
58}
59
60#[derive(Debug, PartialEq, Serialize)]
65#[non_exhaustive]
66pub struct Header {
67 #[serde(skip_serializing_if = "BlockMetadata::is_default")]
68 pub metadata: BlockMetadata,
69 #[serde(skip_serializing_if = "Title::is_empty")]
70 pub title: Title,
71 #[serde(skip_serializing_if = "Option::is_none")]
72 pub subtitle: Option<Subtitle>,
73 #[serde(skip_serializing_if = "Vec::is_empty")]
74 pub authors: Vec<Author>,
75 pub location: Location,
76}
77
78#[derive(Debug, PartialEq, Serialize)]
80#[non_exhaustive]
81pub struct Author {
82 #[serde(rename = "firstname")]
83 pub first_name: String,
84 #[serde(skip_serializing_if = "Option::is_none", rename = "middlename")]
85 pub middle_name: Option<String>,
86 #[serde(rename = "lastname")]
87 pub last_name: String,
88 pub initials: String,
89 #[serde(skip_serializing_if = "Option::is_none", rename = "address")]
90 pub email: Option<String>,
91}
92
93impl Header {
94 #[must_use]
96 pub fn new(title: Title, location: Location) -> Self {
97 Self {
98 metadata: BlockMetadata::default(),
99 title,
100 subtitle: None,
101 authors: Vec::new(),
102 location,
103 }
104 }
105
106 #[must_use]
108 pub fn with_metadata(mut self, metadata: BlockMetadata) -> Self {
109 self.metadata = metadata;
110 self
111 }
112
113 #[must_use]
115 pub fn with_subtitle(mut self, subtitle: Subtitle) -> Self {
116 self.subtitle = Some(subtitle);
117 self
118 }
119
120 #[must_use]
122 pub fn with_authors(mut self, authors: Vec<Author>) -> Self {
123 self.authors = authors;
124 self
125 }
126}
127
128impl Author {
129 #[must_use]
131 pub fn new(first_name: &str, middle_name: Option<&str>, last_name: Option<&str>) -> Self {
132 let first_name = first_name.replace('_', " ");
133 let middle_name = middle_name.map(|m| m.replace('_', " "));
134 let last_name = last_name.map(|l| l.replace('_', " ")).unwrap_or_default();
135 let initials =
136 Self::generate_initials(&first_name, middle_name.as_deref(), Some(&last_name));
137 Self {
138 first_name,
139 middle_name,
140 last_name,
141 initials,
142 email: None,
143 }
144 }
145
146 #[must_use]
148 pub fn with_email(mut self, email: String) -> Self {
149 self.email = Some(email);
150 self
151 }
152
153 fn generate_initials(first: &str, middle: Option<&str>, last: Option<&str>) -> String {
155 let first_initial = first.chars().next().unwrap_or_default().to_string();
156 let middle_initial = middle
157 .map(|m| m.chars().next().unwrap_or_default().to_string())
158 .unwrap_or_default();
159 let last_initial = last
160 .map(|m| m.chars().next().unwrap_or_default().to_string())
161 .unwrap_or_default();
162 first_initial + &middle_initial + &last_initial
163 }
164}
165
166#[derive(Clone, Debug, PartialEq)]
171#[non_exhaustive]
172pub struct Comment {
173 pub content: String,
174 pub location: Location,
175}
176
177#[non_exhaustive]
181#[derive(Clone, Debug, PartialEq, Serialize)]
182#[serde(untagged)]
183pub enum Block {
184 TableOfContents(TableOfContents),
185 Admonition(Admonition),
191 DiscreteHeader(DiscreteHeader),
192 DocumentAttribute(DocumentAttribute),
193 ThematicBreak(ThematicBreak),
194 PageBreak(PageBreak),
195 UnorderedList(UnorderedList),
196 OrderedList(OrderedList),
197 CalloutList(CalloutList),
198 DescriptionList(DescriptionList),
199 Section(Section),
200 DelimitedBlock(DelimitedBlock),
201 Paragraph(Paragraph),
202 Image(Image),
203 Audio(Audio),
204 Video(Video),
205 Comment(Comment),
206}
207
208impl Locateable for Block {
209 fn location(&self) -> &Location {
210 match self {
211 Block::Section(s) => &s.location,
212 Block::Paragraph(p) => &p.location,
213 Block::UnorderedList(l) => &l.location,
214 Block::OrderedList(l) => &l.location,
215 Block::DescriptionList(l) => &l.location,
216 Block::CalloutList(l) => &l.location,
217 Block::DelimitedBlock(d) => &d.location,
218 Block::Admonition(a) => &a.location,
219 Block::TableOfContents(t) => &t.location,
220 Block::DiscreteHeader(h) => &h.location,
221 Block::DocumentAttribute(a) => &a.location,
222 Block::ThematicBreak(tb) => &tb.location,
223 Block::PageBreak(pb) => &pb.location,
224 Block::Image(i) => &i.location,
225 Block::Audio(a) => &a.location,
226 Block::Video(v) => &v.location,
227 Block::Comment(c) => &c.location,
228 }
229 }
230}
231
232#[derive(Clone, Debug, PartialEq)]
237#[non_exhaustive]
238pub struct DocumentAttribute {
239 pub name: AttributeName,
240 pub value: AttributeValue,
241 pub location: Location,
242}
243
244impl Serialize for DocumentAttribute {
245 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
246 where
247 S: Serializer,
248 {
249 let mut state = serializer.serialize_map(None)?;
250 state.serialize_entry("name", &self.name)?;
251 state.serialize_entry("type", "attribute")?;
252 state.serialize_entry("value", &self.value)?;
253 state.serialize_entry("location", &self.location)?;
254 state.end()
255 }
256}
257
258#[derive(Clone, Debug, PartialEq)]
263#[non_exhaustive]
264pub struct DiscreteHeader {
265 pub metadata: BlockMetadata,
266 pub title: Title,
267 pub level: u8,
268 pub location: Location,
269}
270
271#[derive(Clone, Default, Debug, PartialEq)]
273#[non_exhaustive]
274pub struct ThematicBreak {
275 pub anchors: Vec<Anchor>,
276 pub title: Title,
277 pub location: Location,
278}
279
280impl Serialize for ThematicBreak {
281 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
282 where
283 S: Serializer,
284 {
285 let mut state = serializer.serialize_map(None)?;
286 state.serialize_entry("name", "break")?;
287 state.serialize_entry("type", "block")?;
288 state.serialize_entry("variant", "thematic")?;
289 if !self.anchors.is_empty() {
290 state.serialize_entry("anchors", &self.anchors)?;
291 }
292 if !self.title.is_empty() {
293 state.serialize_entry("title", &self.title)?;
294 }
295 state.serialize_entry("location", &self.location)?;
296 state.end()
297 }
298}
299
300#[derive(Clone, Debug, PartialEq)]
302#[non_exhaustive]
303pub struct PageBreak {
304 pub title: Title,
305 pub metadata: BlockMetadata,
306 pub location: Location,
307}
308
309impl Serialize for PageBreak {
310 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
311 where
312 S: Serializer,
313 {
314 let mut state = serializer.serialize_map(None)?;
315 state.serialize_entry("name", "break")?;
316 state.serialize_entry("type", "block")?;
317 state.serialize_entry("variant", "page")?;
318 if !self.title.is_empty() {
319 state.serialize_entry("title", &self.title)?;
320 }
321 if !self.metadata.is_default() {
322 state.serialize_entry("metadata", &self.metadata)?;
323 }
324 state.serialize_entry("location", &self.location)?;
325 state.end()
326 }
327}
328
329impl Serialize for Comment {
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", "comment")?;
336 state.serialize_entry("type", "block")?;
337 if !self.content.is_empty() {
338 state.serialize_entry("content", &self.content)?;
339 }
340 state.serialize_entry("location", &self.location)?;
341 state.end()
342 }
343}
344
345#[derive(Clone, Debug, PartialEq)]
347#[non_exhaustive]
348pub struct TableOfContents {
349 pub metadata: BlockMetadata,
350 pub location: Location,
351}
352
353impl Serialize for TableOfContents {
354 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
355 where
356 S: Serializer,
357 {
358 let mut state = serializer.serialize_map(None)?;
359 state.serialize_entry("name", "toc")?;
360 state.serialize_entry("type", "block")?;
361 if !self.metadata.is_default() {
362 state.serialize_entry("metadata", &self.metadata)?;
363 }
364 state.serialize_entry("location", &self.location)?;
365 state.end()
366 }
367}
368
369#[derive(Clone, Debug, PartialEq)]
371#[non_exhaustive]
372pub struct Paragraph {
373 pub metadata: BlockMetadata,
374 pub title: Title,
375 pub content: Vec<InlineNode>,
376 pub location: Location,
377}
378
379impl Paragraph {
380 #[must_use]
382 pub fn new(content: Vec<InlineNode>, location: Location) -> Self {
383 Self {
384 metadata: BlockMetadata::default(),
385 title: Title::default(),
386 content,
387 location,
388 }
389 }
390
391 #[must_use]
393 pub fn with_metadata(mut self, metadata: BlockMetadata) -> Self {
394 self.metadata = metadata;
395 self
396 }
397
398 #[must_use]
400 pub fn with_title(mut self, title: Title) -> Self {
401 self.title = title;
402 self
403 }
404}
405
406#[derive(Clone, Debug, PartialEq)]
408#[non_exhaustive]
409pub struct DelimitedBlock {
410 pub metadata: BlockMetadata,
411 pub inner: DelimitedBlockType,
412 pub delimiter: String,
413 pub title: Title,
414 pub location: Location,
415 pub open_delimiter_location: Option<Location>,
416 pub close_delimiter_location: Option<Location>,
417}
418
419impl DelimitedBlock {
420 #[must_use]
422 pub fn new(inner: DelimitedBlockType, delimiter: String, location: Location) -> Self {
423 Self {
424 metadata: BlockMetadata::default(),
425 inner,
426 delimiter,
427 title: Title::default(),
428 location,
429 open_delimiter_location: None,
430 close_delimiter_location: None,
431 }
432 }
433
434 #[must_use]
436 pub fn with_metadata(mut self, metadata: BlockMetadata) -> Self {
437 self.metadata = metadata;
438 self
439 }
440
441 #[must_use]
443 pub fn with_title(mut self, title: Title) -> Self {
444 self.title = title;
445 self
446 }
447}
448
449#[derive(Clone, Debug, PartialEq, Serialize)]
451#[serde(rename_all = "lowercase")]
452pub enum StemNotation {
453 Latexmath,
454 Asciimath,
455}
456
457impl Display for StemNotation {
458 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
459 match self {
460 StemNotation::Latexmath => write!(f, "latexmath"),
461 StemNotation::Asciimath => write!(f, "asciimath"),
462 }
463 }
464}
465
466impl FromStr for StemNotation {
467 type Err = String;
468
469 fn from_str(s: &str) -> Result<Self, Self::Err> {
470 match s {
471 "latexmath" => Ok(Self::Latexmath),
472 "asciimath" => Ok(Self::Asciimath),
473 _ => Err(format!("unknown stem notation: {s}")),
474 }
475 }
476}
477
478#[derive(Clone, Debug, PartialEq, Serialize)]
480#[non_exhaustive]
481pub struct StemContent {
482 pub content: String,
483 pub notation: StemNotation,
484}
485
486impl StemContent {
487 #[must_use]
489 pub fn new(content: String, notation: StemNotation) -> Self {
490 Self { content, notation }
491 }
492}
493
494#[non_exhaustive]
531#[derive(Clone, Debug, PartialEq, Serialize)]
532#[serde(untagged)]
533pub enum DelimitedBlockType {
534 DelimitedComment(Vec<InlineNode>),
536 DelimitedExample(Vec<Block>),
538 DelimitedListing(Vec<InlineNode>),
540 DelimitedLiteral(Vec<InlineNode>),
542 DelimitedOpen(Vec<Block>),
544 DelimitedSidebar(Vec<Block>),
546 DelimitedTable(Table),
548 DelimitedPass(Vec<InlineNode>),
550 DelimitedQuote(Vec<Block>),
552 DelimitedVerse(Vec<InlineNode>),
554 DelimitedStem(StemContent),
556}
557
558impl DelimitedBlockType {
559 fn name(&self) -> &'static str {
560 match self {
561 DelimitedBlockType::DelimitedComment(_) => "comment",
562 DelimitedBlockType::DelimitedExample(_) => "example",
563 DelimitedBlockType::DelimitedListing(_) => "listing",
564 DelimitedBlockType::DelimitedLiteral(_) => "literal",
565 DelimitedBlockType::DelimitedOpen(_) => "open",
566 DelimitedBlockType::DelimitedSidebar(_) => "sidebar",
567 DelimitedBlockType::DelimitedTable(_) => "table",
568 DelimitedBlockType::DelimitedPass(_) => "pass",
569 DelimitedBlockType::DelimitedQuote(_) => "quote",
570 DelimitedBlockType::DelimitedVerse(_) => "verse",
571 DelimitedBlockType::DelimitedStem(_) => "stem",
572 }
573 }
574}
575
576impl Serialize for Document {
577 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
578 where
579 S: Serializer,
580 {
581 let mut state = serializer.serialize_map(None)?;
582 state.serialize_entry("name", "document")?;
583 state.serialize_entry("type", "block")?;
584 if let Some(header) = &self.header {
585 state.serialize_entry("header", header)?;
586 state.serialize_entry("attributes", &self.attributes)?;
589 } else if !self.attributes.is_empty() {
590 state.serialize_entry("attributes", &self.attributes)?;
591 }
592 if !self.blocks.is_empty() {
593 state.serialize_entry("blocks", &self.blocks)?;
594 }
595 state.serialize_entry("location", &self.location)?;
596 state.end()
597 }
598}
599
600impl Serialize for DelimitedBlock {
601 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
602 where
603 S: Serializer,
604 {
605 let mut state = serializer.serialize_map(None)?;
606 state.serialize_entry("name", self.inner.name())?;
607 state.serialize_entry("type", "block")?;
608 state.serialize_entry("form", "delimited")?;
609 state.serialize_entry("delimiter", &self.delimiter)?;
610 if !self.metadata.is_default() {
611 state.serialize_entry("metadata", &self.metadata)?;
612 }
613
614 match &self.inner {
615 DelimitedBlockType::DelimitedStem(stem) => {
616 state.serialize_entry("content", &stem.content)?;
617 state.serialize_entry("notation", &stem.notation)?;
618 }
619 DelimitedBlockType::DelimitedListing(inner)
620 | DelimitedBlockType::DelimitedLiteral(inner)
621 | DelimitedBlockType::DelimitedPass(inner)
622 | DelimitedBlockType::DelimitedVerse(inner) => {
623 state.serialize_entry("inlines", &inner)?;
624 }
625 DelimitedBlockType::DelimitedTable(inner) => {
626 state.serialize_entry("content", &inner)?;
627 }
628 inner @ (DelimitedBlockType::DelimitedComment(_)
629 | DelimitedBlockType::DelimitedExample(_)
630 | DelimitedBlockType::DelimitedOpen(_)
631 | DelimitedBlockType::DelimitedQuote(_)
632 | DelimitedBlockType::DelimitedSidebar(_)) => {
633 state.serialize_entry("blocks", &inner)?;
634 }
635 }
636 if !self.title.is_empty() {
637 state.serialize_entry("title", &self.title)?;
638 }
639 state.serialize_entry("location", &self.location)?;
640 state.end()
641 }
642}
643
644impl Serialize for DiscreteHeader {
645 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
646 where
647 S: Serializer,
648 {
649 let mut state = serializer.serialize_map(None)?;
650 state.serialize_entry("name", "heading")?;
651 state.serialize_entry("type", "block")?;
652 if !self.title.is_empty() {
653 state.serialize_entry("title", &self.title)?;
654 }
655 state.serialize_entry("level", &self.level)?;
656 if !self.metadata.is_default() {
657 state.serialize_entry("metadata", &self.metadata)?;
658 }
659 state.serialize_entry("location", &self.location)?;
660 state.end()
661 }
662}
663
664impl Serialize for Paragraph {
665 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
666 where
667 S: Serializer,
668 {
669 let mut state = serializer.serialize_map(None)?;
670 state.serialize_entry("name", "paragraph")?;
671 state.serialize_entry("type", "block")?;
672 if !self.title.is_empty() {
673 state.serialize_entry("title", &self.title)?;
674 }
675 state.serialize_entry("inlines", &self.content)?;
676 if !self.metadata.is_default() {
677 state.serialize_entry("metadata", &self.metadata)?;
678 }
679 state.serialize_entry("location", &self.location)?;
680 state.end()
681 }
682}