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,
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 initials = Self::generate_initials(first_name, middle_name, last_name);
133 let last_name = last_name.unwrap_or_default().to_string();
134 Self {
135 first_name: first_name.to_string(),
136 middle_name: middle_name.map(ToString::to_string),
137 last_name,
138 initials,
139 email: None,
140 }
141 }
142
143 #[must_use]
145 pub fn with_email(mut self, email: String) -> Self {
146 self.email = Some(email);
147 self
148 }
149
150 fn generate_initials(first: &str, middle: Option<&str>, last: Option<&str>) -> String {
152 let first_initial = first.chars().next().unwrap_or_default().to_string();
153 let middle_initial = middle
154 .map(|m| m.chars().next().unwrap_or_default().to_string())
155 .unwrap_or_default();
156 let last_initial = last
157 .map(|m| m.chars().next().unwrap_or_default().to_string())
158 .unwrap_or_default();
159 first_initial + &middle_initial + &last_initial
160 }
161}
162
163#[derive(Clone, Debug, PartialEq)]
168#[non_exhaustive]
169pub struct Comment {
170 pub content: String,
171 pub location: Location,
172}
173
174#[non_exhaustive]
178#[derive(Clone, Debug, PartialEq, Serialize)]
179#[serde(untagged)]
180pub enum Block {
181 TableOfContents(TableOfContents),
182 Admonition(Admonition),
188 DiscreteHeader(DiscreteHeader),
189 DocumentAttribute(DocumentAttribute),
190 ThematicBreak(ThematicBreak),
191 PageBreak(PageBreak),
192 UnorderedList(UnorderedList),
193 OrderedList(OrderedList),
194 CalloutList(CalloutList),
195 DescriptionList(DescriptionList),
196 Section(Section),
197 DelimitedBlock(DelimitedBlock),
198 Paragraph(Paragraph),
199 Image(Image),
200 Audio(Audio),
201 Video(Video),
202 Comment(Comment),
203}
204
205impl Locateable for Block {
206 fn location(&self) -> &Location {
207 match self {
208 Block::Section(s) => &s.location,
209 Block::Paragraph(p) => &p.location,
210 Block::UnorderedList(l) => &l.location,
211 Block::OrderedList(l) => &l.location,
212 Block::DescriptionList(l) => &l.location,
213 Block::CalloutList(l) => &l.location,
214 Block::DelimitedBlock(d) => &d.location,
215 Block::Admonition(a) => &a.location,
216 Block::TableOfContents(t) => &t.location,
217 Block::DiscreteHeader(h) => &h.location,
218 Block::DocumentAttribute(a) => &a.location,
219 Block::ThematicBreak(tb) => &tb.location,
220 Block::PageBreak(pb) => &pb.location,
221 Block::Image(i) => &i.location,
222 Block::Audio(a) => &a.location,
223 Block::Video(v) => &v.location,
224 Block::Comment(c) => &c.location,
225 }
226 }
227}
228
229#[derive(Clone, Debug, PartialEq)]
234#[non_exhaustive]
235pub struct DocumentAttribute {
236 pub name: AttributeName,
237 pub value: AttributeValue,
238 pub location: Location,
239}
240
241impl Serialize for DocumentAttribute {
242 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
243 where
244 S: Serializer,
245 {
246 let mut state = serializer.serialize_map(None)?;
247 state.serialize_entry("name", &self.name)?;
248 state.serialize_entry("type", "attribute")?;
249 state.serialize_entry("value", &self.value)?;
250 state.serialize_entry("location", &self.location)?;
251 state.end()
252 }
253}
254
255#[derive(Clone, Debug, PartialEq)]
260#[non_exhaustive]
261pub struct DiscreteHeader {
262 pub metadata: BlockMetadata,
263 pub title: Title,
264 pub level: u8,
265 pub location: Location,
266}
267
268#[derive(Clone, Default, Debug, PartialEq)]
270#[non_exhaustive]
271pub struct ThematicBreak {
272 pub anchors: Vec<Anchor>,
273 pub title: Title,
274 pub location: Location,
275}
276
277impl Serialize for ThematicBreak {
278 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
279 where
280 S: Serializer,
281 {
282 let mut state = serializer.serialize_map(None)?;
283 state.serialize_entry("name", "break")?;
284 state.serialize_entry("type", "block")?;
285 state.serialize_entry("variant", "thematic")?;
286 if !self.anchors.is_empty() {
287 state.serialize_entry("anchors", &self.anchors)?;
288 }
289 if !self.title.is_empty() {
290 state.serialize_entry("title", &self.title)?;
291 }
292 state.serialize_entry("location", &self.location)?;
293 state.end()
294 }
295}
296
297#[derive(Clone, Debug, PartialEq)]
299#[non_exhaustive]
300pub struct PageBreak {
301 pub title: Title,
302 pub metadata: BlockMetadata,
303 pub location: Location,
304}
305
306impl Serialize for PageBreak {
307 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
308 where
309 S: Serializer,
310 {
311 let mut state = serializer.serialize_map(None)?;
312 state.serialize_entry("name", "break")?;
313 state.serialize_entry("type", "block")?;
314 state.serialize_entry("variant", "page")?;
315 if !self.title.is_empty() {
316 state.serialize_entry("title", &self.title)?;
317 }
318 if !self.metadata.is_default() {
319 state.serialize_entry("metadata", &self.metadata)?;
320 }
321 state.serialize_entry("location", &self.location)?;
322 state.end()
323 }
324}
325
326impl Serialize for Comment {
327 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
328 where
329 S: Serializer,
330 {
331 let mut state = serializer.serialize_map(None)?;
332 state.serialize_entry("name", "comment")?;
333 state.serialize_entry("type", "block")?;
334 if !self.content.is_empty() {
335 state.serialize_entry("content", &self.content)?;
336 }
337 state.serialize_entry("location", &self.location)?;
338 state.end()
339 }
340}
341
342#[derive(Clone, Debug, PartialEq)]
344#[non_exhaustive]
345pub struct TableOfContents {
346 pub metadata: BlockMetadata,
347 pub location: Location,
348}
349
350impl Serialize for TableOfContents {
351 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
352 where
353 S: Serializer,
354 {
355 let mut state = serializer.serialize_map(None)?;
356 state.serialize_entry("name", "toc")?;
357 state.serialize_entry("type", "block")?;
358 if !self.metadata.is_default() {
359 state.serialize_entry("metadata", &self.metadata)?;
360 }
361 state.serialize_entry("location", &self.location)?;
362 state.end()
363 }
364}
365
366#[derive(Clone, Debug, PartialEq)]
368#[non_exhaustive]
369pub struct Paragraph {
370 pub metadata: BlockMetadata,
371 pub title: Title,
372 pub content: Vec<InlineNode>,
373 pub location: Location,
374}
375
376impl Paragraph {
377 #[must_use]
379 pub fn new(content: Vec<InlineNode>, location: Location) -> Self {
380 Self {
381 metadata: BlockMetadata::default(),
382 title: Title::default(),
383 content,
384 location,
385 }
386 }
387
388 #[must_use]
390 pub fn with_metadata(mut self, metadata: BlockMetadata) -> Self {
391 self.metadata = metadata;
392 self
393 }
394
395 #[must_use]
397 pub fn with_title(mut self, title: Title) -> Self {
398 self.title = title;
399 self
400 }
401}
402
403#[derive(Clone, Debug, PartialEq)]
405#[non_exhaustive]
406pub struct DelimitedBlock {
407 pub metadata: BlockMetadata,
408 pub inner: DelimitedBlockType,
409 pub delimiter: String,
410 pub title: Title,
411 pub location: Location,
412 pub open_delimiter_location: Option<Location>,
413 pub close_delimiter_location: Option<Location>,
414}
415
416impl DelimitedBlock {
417 #[must_use]
419 pub fn new(inner: DelimitedBlockType, delimiter: String, location: Location) -> Self {
420 Self {
421 metadata: BlockMetadata::default(),
422 inner,
423 delimiter,
424 title: Title::default(),
425 location,
426 open_delimiter_location: None,
427 close_delimiter_location: None,
428 }
429 }
430
431 #[must_use]
433 pub fn with_metadata(mut self, metadata: BlockMetadata) -> Self {
434 self.metadata = metadata;
435 self
436 }
437
438 #[must_use]
440 pub fn with_title(mut self, title: Title) -> Self {
441 self.title = title;
442 self
443 }
444}
445
446#[derive(Clone, Debug, PartialEq, Serialize)]
448#[serde(rename_all = "lowercase")]
449pub enum StemNotation {
450 Latexmath,
451 Asciimath,
452}
453
454impl Display for StemNotation {
455 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
456 match self {
457 StemNotation::Latexmath => write!(f, "latexmath"),
458 StemNotation::Asciimath => write!(f, "asciimath"),
459 }
460 }
461}
462
463impl FromStr for StemNotation {
464 type Err = String;
465
466 fn from_str(s: &str) -> Result<Self, Self::Err> {
467 match s {
468 "latexmath" => Ok(Self::Latexmath),
469 "asciimath" => Ok(Self::Asciimath),
470 _ => Err(format!("unknown stem notation: {s}")),
471 }
472 }
473}
474
475#[derive(Clone, Debug, PartialEq, Serialize)]
477#[non_exhaustive]
478pub struct StemContent {
479 pub content: String,
480 pub notation: StemNotation,
481}
482
483impl StemContent {
484 #[must_use]
486 pub fn new(content: String, notation: StemNotation) -> Self {
487 Self { content, notation }
488 }
489}
490
491#[non_exhaustive]
528#[derive(Clone, Debug, PartialEq, Serialize)]
529#[serde(untagged)]
530pub enum DelimitedBlockType {
531 DelimitedComment(Vec<InlineNode>),
533 DelimitedExample(Vec<Block>),
535 DelimitedListing(Vec<InlineNode>),
537 DelimitedLiteral(Vec<InlineNode>),
539 DelimitedOpen(Vec<Block>),
541 DelimitedSidebar(Vec<Block>),
543 DelimitedTable(Table),
545 DelimitedPass(Vec<InlineNode>),
547 DelimitedQuote(Vec<Block>),
549 DelimitedVerse(Vec<InlineNode>),
551 DelimitedStem(StemContent),
553}
554
555impl DelimitedBlockType {
556 fn name(&self) -> &'static str {
557 match self {
558 DelimitedBlockType::DelimitedComment(_) => "comment",
559 DelimitedBlockType::DelimitedExample(_) => "example",
560 DelimitedBlockType::DelimitedListing(_) => "listing",
561 DelimitedBlockType::DelimitedLiteral(_) => "literal",
562 DelimitedBlockType::DelimitedOpen(_) => "open",
563 DelimitedBlockType::DelimitedSidebar(_) => "sidebar",
564 DelimitedBlockType::DelimitedTable(_) => "table",
565 DelimitedBlockType::DelimitedPass(_) => "pass",
566 DelimitedBlockType::DelimitedQuote(_) => "quote",
567 DelimitedBlockType::DelimitedVerse(_) => "verse",
568 DelimitedBlockType::DelimitedStem(_) => "stem",
569 }
570 }
571}
572
573impl Serialize for Document {
574 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
575 where
576 S: Serializer,
577 {
578 let mut state = serializer.serialize_map(None)?;
579 state.serialize_entry("name", "document")?;
580 state.serialize_entry("type", "block")?;
581 if let Some(header) = &self.header {
582 state.serialize_entry("header", header)?;
583 state.serialize_entry("attributes", &self.attributes)?;
586 } else if !self.attributes.is_empty() {
587 state.serialize_entry("attributes", &self.attributes)?;
588 }
589 if !self.blocks.is_empty() {
590 state.serialize_entry("blocks", &self.blocks)?;
591 }
592 state.serialize_entry("location", &self.location)?;
593 state.end()
594 }
595}
596
597impl Serialize for DelimitedBlock {
598 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
599 where
600 S: Serializer,
601 {
602 let mut state = serializer.serialize_map(None)?;
603 state.serialize_entry("name", self.inner.name())?;
604 state.serialize_entry("type", "block")?;
605 state.serialize_entry("form", "delimited")?;
606 state.serialize_entry("delimiter", &self.delimiter)?;
607 if !self.metadata.is_default() {
608 state.serialize_entry("metadata", &self.metadata)?;
609 }
610
611 match &self.inner {
612 DelimitedBlockType::DelimitedStem(stem) => {
613 state.serialize_entry("content", &stem.content)?;
614 state.serialize_entry("notation", &stem.notation)?;
615 }
616 DelimitedBlockType::DelimitedListing(inner)
617 | DelimitedBlockType::DelimitedLiteral(inner)
618 | DelimitedBlockType::DelimitedPass(inner)
619 | DelimitedBlockType::DelimitedVerse(inner) => {
620 state.serialize_entry("inlines", &inner)?;
621 }
622 DelimitedBlockType::DelimitedTable(inner) => {
623 state.serialize_entry("content", &inner)?;
624 }
625 inner @ (DelimitedBlockType::DelimitedComment(_)
626 | DelimitedBlockType::DelimitedExample(_)
627 | DelimitedBlockType::DelimitedOpen(_)
628 | DelimitedBlockType::DelimitedQuote(_)
629 | DelimitedBlockType::DelimitedSidebar(_)) => {
630 state.serialize_entry("blocks", &inner)?;
631 }
632 }
633 if !self.title.is_empty() {
634 state.serialize_entry("title", &self.title)?;
635 }
636 state.serialize_entry("location", &self.location)?;
637 state.end()
638 }
639}
640
641impl Serialize for DiscreteHeader {
642 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
643 where
644 S: Serializer,
645 {
646 let mut state = serializer.serialize_map(None)?;
647 state.serialize_entry("name", "heading")?;
648 state.serialize_entry("type", "block")?;
649 if !self.title.is_empty() {
650 state.serialize_entry("title", &self.title)?;
651 }
652 state.serialize_entry("level", &self.level)?;
653 if !self.metadata.is_default() {
654 state.serialize_entry("metadata", &self.metadata)?;
655 }
656 state.serialize_entry("location", &self.location)?;
657 state.end()
658 }
659}
660
661impl Serialize for Paragraph {
662 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
663 where
664 S: Serializer,
665 {
666 let mut state = serializer.serialize_map(None)?;
667 state.serialize_entry("name", "paragraph")?;
668 state.serialize_entry("type", "block")?;
669 if !self.title.is_empty() {
670 state.serialize_entry("title", &self.title)?;
671 }
672 state.serialize_entry("inlines", &self.content)?;
673 if !self.metadata.is_default() {
674 state.serialize_entry("metadata", &self.metadata)?;
675 }
676 state.serialize_entry("location", &self.location)?;
677 state.end()
678 }
679}