1use std::{fmt::Display, str::FromStr, string::ToString};
3
4use serde::{
5 Deserialize, Serialize,
6 de::{self, Deserializer},
7 ser::{SerializeMap, Serializer},
8};
9
10mod admonition;
11mod anchor;
12mod attributes;
13mod inlines;
14mod lists;
15mod location;
16mod media;
17mod metadata;
18mod section;
19mod substitution;
20mod tables;
21mod title;
22
23pub use admonition::{Admonition, AdmonitionVariant};
24pub use anchor::{Anchor, TocEntry};
25pub use attributes::{AttributeName, AttributeValue, DocumentAttributes, ElementAttributes};
26pub use inlines::*;
27pub use lists::{
28 CalloutList, DescriptionList, DescriptionListItem, ListItem, ListItemCheckedStatus, ListLevel,
29 OrderedList, UnorderedList,
30};
31pub use location::*;
32pub use media::{Audio, Image, Source, Video};
33pub use metadata::{BlockMetadata, Role};
34pub use section::*;
35pub use substitution::*;
36pub use tables::{
37 ColumnFormat, ColumnStyle, ColumnWidth, HorizontalAlignment, Table, TableColumn, TableRow,
38 VerticalAlignment,
39};
40pub use title::{Subtitle, Title};
41
42#[derive(Default, Debug, PartialEq, Deserialize)]
44#[non_exhaustive]
45pub struct Document {
46 pub(crate) name: String,
47 pub(crate) r#type: String,
48 #[serde(default)]
49 pub header: Option<Header>,
50 #[serde(default, skip_serializing_if = "DocumentAttributes::is_empty")]
51 pub attributes: DocumentAttributes,
52 #[serde(default)]
53 pub blocks: Vec<Block>,
54 #[serde(skip)]
55 pub footnotes: Vec<Footnote>,
56 #[serde(skip)]
57 pub toc_entries: Vec<TocEntry>,
58 pub location: Location,
59}
60
61#[derive(Debug, PartialEq, Serialize, Deserialize)]
66#[non_exhaustive]
67pub struct Header {
68 #[serde(default, skip_serializing_if = "BlockMetadata::is_default")]
69 pub metadata: BlockMetadata,
70 #[serde(default, skip_serializing_if = "Title::is_empty")]
71 pub title: Title,
72 #[serde(default, skip_serializing_if = "Option::is_none")]
73 pub subtitle: Option<Subtitle>,
74 #[serde(default, skip_serializing_if = "Vec::is_empty")]
75 pub authors: Vec<Author>,
76 pub location: Location,
77}
78
79#[derive(Debug, PartialEq, Serialize, Deserialize)]
81#[non_exhaustive]
82pub struct Author {
83 #[serde(rename = "firstname")]
84 pub first_name: String,
85 #[serde(
86 default,
87 skip_serializing_if = "Option::is_none",
88 rename = "middlename"
89 )]
90 pub middle_name: Option<String>,
91 #[serde(rename = "lastname")]
92 pub last_name: String,
93 pub initials: String,
94 #[serde(default, skip_serializing_if = "Option::is_none", rename = "address")]
95 pub email: Option<String>,
96}
97
98impl Header {
99 #[must_use]
101 pub fn new(title: Title, location: Location) -> Self {
102 Self {
103 metadata: BlockMetadata::default(),
104 title,
105 subtitle: None,
106 authors: Vec::new(),
107 location,
108 }
109 }
110
111 #[must_use]
113 pub fn with_metadata(mut self, metadata: BlockMetadata) -> Self {
114 self.metadata = metadata;
115 self
116 }
117
118 #[must_use]
120 pub fn with_subtitle(mut self, subtitle: Subtitle) -> Self {
121 self.subtitle = Some(subtitle);
122 self
123 }
124
125 #[must_use]
127 pub fn with_authors(mut self, authors: Vec<Author>) -> Self {
128 self.authors = authors;
129 self
130 }
131}
132
133impl Author {
134 #[must_use]
136 pub fn new(first_name: &str, middle_name: Option<&str>, last_name: Option<&str>) -> Self {
137 let initials = Self::generate_initials(first_name, middle_name, last_name);
138 let last_name = last_name.unwrap_or_default().to_string();
139 Self {
140 first_name: first_name.to_string(),
141 middle_name: middle_name.map(ToString::to_string),
142 last_name,
143 initials,
144 email: None,
145 }
146 }
147
148 #[must_use]
150 pub fn with_email(mut self, email: String) -> Self {
151 self.email = Some(email);
152 self
153 }
154
155 fn generate_initials(first: &str, middle: Option<&str>, last: Option<&str>) -> String {
157 let first_initial = first.chars().next().unwrap_or_default().to_string();
158 let middle_initial = middle
159 .map(|m| m.chars().next().unwrap_or_default().to_string())
160 .unwrap_or_default();
161 let last_initial = last
162 .map(|m| m.chars().next().unwrap_or_default().to_string())
163 .unwrap_or_default();
164 first_initial + &middle_initial + &last_initial
165 }
166}
167
168#[derive(Clone, Debug, PartialEq)]
173#[non_exhaustive]
174pub struct Comment {
175 pub content: String,
176 pub location: Location,
177}
178
179#[non_exhaustive]
183#[derive(Clone, Debug, PartialEq, Serialize)]
184#[serde(untagged)]
185pub enum Block {
186 TableOfContents(TableOfContents),
187 Admonition(Admonition),
193 DiscreteHeader(DiscreteHeader),
194 DocumentAttribute(DocumentAttribute),
195 ThematicBreak(ThematicBreak),
196 PageBreak(PageBreak),
197 UnorderedList(UnorderedList),
198 OrderedList(OrderedList),
199 CalloutList(CalloutList),
200 DescriptionList(DescriptionList),
201 Section(Section),
202 DelimitedBlock(DelimitedBlock),
203 Paragraph(Paragraph),
204 Image(Image),
205 Audio(Audio),
206 Video(Video),
207 Comment(Comment),
208}
209
210impl Locateable for Block {
211 fn location(&self) -> &Location {
212 match self {
213 Block::Section(s) => &s.location,
214 Block::Paragraph(p) => &p.location,
215 Block::UnorderedList(l) => &l.location,
216 Block::OrderedList(l) => &l.location,
217 Block::DescriptionList(l) => &l.location,
218 Block::CalloutList(l) => &l.location,
219 Block::DelimitedBlock(d) => &d.location,
220 Block::Admonition(a) => &a.location,
221 Block::TableOfContents(t) => &t.location,
222 Block::DiscreteHeader(h) => &h.location,
223 Block::DocumentAttribute(a) => &a.location,
224 Block::ThematicBreak(tb) => &tb.location,
225 Block::PageBreak(pb) => &pb.location,
226 Block::Image(i) => &i.location,
227 Block::Audio(a) => &a.location,
228 Block::Video(v) => &v.location,
229 Block::Comment(c) => &c.location,
230 }
231 }
232}
233
234#[derive(Clone, Debug, PartialEq)]
239#[non_exhaustive]
240pub struct DocumentAttribute {
241 pub name: AttributeName,
242 pub value: AttributeValue,
243 pub location: Location,
244}
245
246impl Serialize for DocumentAttribute {
247 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
248 where
249 S: Serializer,
250 {
251 let mut state = serializer.serialize_map(None)?;
252 state.serialize_entry("name", &self.name)?;
253 state.serialize_entry("type", "attribute")?;
254 state.serialize_entry("value", &self.value)?;
255 state.serialize_entry("location", &self.location)?;
256 state.end()
257 }
258}
259
260#[derive(Clone, Debug, PartialEq)]
265#[non_exhaustive]
266pub struct DiscreteHeader {
267 pub metadata: BlockMetadata,
268 pub title: Title,
269 pub level: u8,
270 pub location: Location,
271}
272
273#[derive(Clone, Default, Debug, PartialEq)]
275#[non_exhaustive]
276pub struct ThematicBreak {
277 pub anchors: Vec<Anchor>,
278 pub title: Title,
279 pub location: Location,
280}
281
282impl Serialize for ThematicBreak {
283 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
284 where
285 S: Serializer,
286 {
287 let mut state = serializer.serialize_map(None)?;
288 state.serialize_entry("name", "break")?;
289 state.serialize_entry("type", "block")?;
290 state.serialize_entry("variant", "thematic")?;
291 if !self.anchors.is_empty() {
292 state.serialize_entry("anchors", &self.anchors)?;
293 }
294 if !self.title.is_empty() {
295 state.serialize_entry("title", &self.title)?;
296 }
297 state.serialize_entry("location", &self.location)?;
298 state.end()
299 }
300}
301
302#[derive(Clone, Debug, PartialEq)]
304#[non_exhaustive]
305pub struct PageBreak {
306 pub title: Title,
307 pub metadata: BlockMetadata,
308 pub location: Location,
309}
310
311impl Serialize for PageBreak {
312 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
313 where
314 S: Serializer,
315 {
316 let mut state = serializer.serialize_map(None)?;
317 state.serialize_entry("name", "break")?;
318 state.serialize_entry("type", "block")?;
319 state.serialize_entry("variant", "page")?;
320 if !self.title.is_empty() {
321 state.serialize_entry("title", &self.title)?;
322 }
323 if !self.metadata.is_default() {
324 state.serialize_entry("metadata", &self.metadata)?;
325 }
326 state.serialize_entry("location", &self.location)?;
327 state.end()
328 }
329}
330
331impl Serialize for Comment {
332 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
333 where
334 S: Serializer,
335 {
336 let mut state = serializer.serialize_map(None)?;
337 state.serialize_entry("name", "comment")?;
338 state.serialize_entry("type", "block")?;
339 if !self.content.is_empty() {
340 state.serialize_entry("content", &self.content)?;
341 }
342 state.serialize_entry("location", &self.location)?;
343 state.end()
344 }
345}
346
347#[derive(Clone, Debug, PartialEq)]
349#[non_exhaustive]
350pub struct TableOfContents {
351 pub metadata: BlockMetadata,
352 pub location: Location,
353}
354
355impl Serialize for TableOfContents {
356 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
357 where
358 S: Serializer,
359 {
360 let mut state = serializer.serialize_map(None)?;
361 state.serialize_entry("name", "toc")?;
362 state.serialize_entry("type", "block")?;
363 if !self.metadata.is_default() {
364 state.serialize_entry("metadata", &self.metadata)?;
365 }
366 state.serialize_entry("location", &self.location)?;
367 state.end()
368 }
369}
370
371#[derive(Clone, Debug, PartialEq)]
373#[non_exhaustive]
374pub struct Paragraph {
375 pub metadata: BlockMetadata,
376 pub title: Title,
377 pub content: Vec<InlineNode>,
378 pub location: Location,
379}
380
381impl Paragraph {
382 #[must_use]
384 pub fn new(content: Vec<InlineNode>, location: Location) -> Self {
385 Self {
386 metadata: BlockMetadata::default(),
387 title: Title::default(),
388 content,
389 location,
390 }
391 }
392
393 #[must_use]
395 pub fn with_metadata(mut self, metadata: BlockMetadata) -> Self {
396 self.metadata = metadata;
397 self
398 }
399
400 #[must_use]
402 pub fn with_title(mut self, title: Title) -> Self {
403 self.title = title;
404 self
405 }
406}
407
408#[derive(Clone, Debug, PartialEq)]
410#[non_exhaustive]
411pub struct DelimitedBlock {
412 pub metadata: BlockMetadata,
413 pub inner: DelimitedBlockType,
414 pub delimiter: String,
415 pub title: Title,
416 pub location: 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 }
430 }
431
432 #[must_use]
434 pub fn with_metadata(mut self, metadata: BlockMetadata) -> Self {
435 self.metadata = metadata;
436 self
437 }
438
439 #[must_use]
441 pub fn with_title(mut self, title: Title) -> Self {
442 self.title = title;
443 self
444 }
445}
446
447#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
449#[serde(rename_all = "lowercase")]
450pub enum StemNotation {
451 Latexmath,
452 Asciimath,
453}
454
455impl Display for StemNotation {
456 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
457 match self {
458 StemNotation::Latexmath => write!(f, "latexmath"),
459 StemNotation::Asciimath => write!(f, "asciimath"),
460 }
461 }
462}
463
464impl FromStr for StemNotation {
465 type Err = String;
466
467 fn from_str(s: &str) -> Result<Self, Self::Err> {
468 match s {
469 "latexmath" => Ok(Self::Latexmath),
470 "asciimath" => Ok(Self::Asciimath),
471 _ => Err(format!("unknown stem notation: {s}")),
472 }
473 }
474}
475
476#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
478#[non_exhaustive]
479pub struct StemContent {
480 pub content: String,
481 pub notation: StemNotation,
482}
483
484impl StemContent {
485 #[must_use]
487 pub fn new(content: String, notation: StemNotation) -> Self {
488 Self { content, notation }
489 }
490}
491
492#[non_exhaustive]
494#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
495#[serde(untagged)]
496pub enum DelimitedBlockType {
497 DelimitedComment(Vec<InlineNode>),
498 DelimitedExample(Vec<Block>),
499 DelimitedListing(Vec<InlineNode>),
500 DelimitedLiteral(Vec<InlineNode>),
501 DelimitedOpen(Vec<Block>),
502 DelimitedSidebar(Vec<Block>),
503 DelimitedTable(Table),
504 DelimitedPass(Vec<InlineNode>),
505 DelimitedQuote(Vec<Block>),
506 DelimitedVerse(Vec<InlineNode>),
507 DelimitedStem(StemContent),
508}
509
510impl DelimitedBlockType {
511 fn name(&self) -> &'static str {
512 match self {
513 DelimitedBlockType::DelimitedComment(_) => "comment",
514 DelimitedBlockType::DelimitedExample(_) => "example",
515 DelimitedBlockType::DelimitedListing(_) => "listing",
516 DelimitedBlockType::DelimitedLiteral(_) => "literal",
517 DelimitedBlockType::DelimitedOpen(_) => "open",
518 DelimitedBlockType::DelimitedSidebar(_) => "sidebar",
519 DelimitedBlockType::DelimitedTable(_) => "table",
520 DelimitedBlockType::DelimitedPass(_) => "pass",
521 DelimitedBlockType::DelimitedQuote(_) => "quote",
522 DelimitedBlockType::DelimitedVerse(_) => "verse",
523 DelimitedBlockType::DelimitedStem(_) => "stem",
524 }
525 }
526}
527
528impl Serialize for Document {
529 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
530 where
531 S: Serializer,
532 {
533 let mut state = serializer.serialize_map(None)?;
534 state.serialize_entry("name", "document")?;
535 state.serialize_entry("type", "block")?;
536 if let Some(header) = &self.header {
537 state.serialize_entry("header", header)?;
538 state.serialize_entry("attributes", &self.attributes)?;
541 } else if !self.attributes.is_empty() {
542 state.serialize_entry("attributes", &self.attributes)?;
543 }
544 if !self.blocks.is_empty() {
545 state.serialize_entry("blocks", &self.blocks)?;
546 }
547 state.serialize_entry("location", &self.location)?;
548 state.end()
549 }
550}
551
552impl Serialize for DelimitedBlock {
553 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
554 where
555 S: Serializer,
556 {
557 let mut state = serializer.serialize_map(None)?;
558 state.serialize_entry("name", self.inner.name())?;
559 state.serialize_entry("type", "block")?;
560 state.serialize_entry("form", "delimited")?;
561 state.serialize_entry("delimiter", &self.delimiter)?;
562 if !self.metadata.is_default() {
563 state.serialize_entry("metadata", &self.metadata)?;
564 }
565
566 match &self.inner {
567 DelimitedBlockType::DelimitedStem(stem) => {
568 state.serialize_entry("content", &stem.content)?;
569 state.serialize_entry("notation", &stem.notation)?;
570 }
571 DelimitedBlockType::DelimitedListing(inner)
572 | DelimitedBlockType::DelimitedLiteral(inner)
573 | DelimitedBlockType::DelimitedPass(inner)
574 | DelimitedBlockType::DelimitedVerse(inner) => {
575 state.serialize_entry("inlines", &inner)?;
576 }
577 DelimitedBlockType::DelimitedTable(inner) => {
578 state.serialize_entry("content", &inner)?;
579 }
580 inner @ (DelimitedBlockType::DelimitedComment(_)
581 | DelimitedBlockType::DelimitedExample(_)
582 | DelimitedBlockType::DelimitedOpen(_)
583 | DelimitedBlockType::DelimitedQuote(_)
584 | DelimitedBlockType::DelimitedSidebar(_)) => {
585 state.serialize_entry("blocks", &inner)?;
586 }
587 }
588 if !self.title.is_empty() {
589 state.serialize_entry("title", &self.title)?;
590 }
591 state.serialize_entry("location", &self.location)?;
592 state.end()
593 }
594}
595
596impl Serialize for DiscreteHeader {
597 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
598 where
599 S: Serializer,
600 {
601 let mut state = serializer.serialize_map(None)?;
602 state.serialize_entry("name", "heading")?;
603 state.serialize_entry("type", "block")?;
604 if !self.title.is_empty() {
605 state.serialize_entry("title", &self.title)?;
606 }
607 state.serialize_entry("level", &self.level)?;
608 if !self.metadata.is_default() {
609 state.serialize_entry("metadata", &self.metadata)?;
610 }
611 state.serialize_entry("location", &self.location)?;
612 state.end()
613 }
614}
615
616impl Serialize for Paragraph {
617 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
618 where
619 S: Serializer,
620 {
621 let mut state = serializer.serialize_map(None)?;
622 state.serialize_entry("name", "paragraph")?;
623 state.serialize_entry("type", "block")?;
624 if !self.title.is_empty() {
625 state.serialize_entry("title", &self.title)?;
626 }
627 state.serialize_entry("inlines", &self.content)?;
628 if !self.metadata.is_default() {
629 state.serialize_entry("metadata", &self.metadata)?;
630 }
631 state.serialize_entry("location", &self.location)?;
632 state.end()
633 }
634}
635
636#[derive(Default, Deserialize)]
643#[serde(default)]
644struct RawBlockFields {
645 name: Option<String>,
646 r#type: Option<String>,
647 value: Option<String>,
648 form: Option<String>,
649 target: Option<String>,
650 source: Option<Source>,
651 sources: Option<Vec<Source>>,
652 delimiter: Option<String>,
653 reftext: Option<String>,
654 id: Option<String>,
655 title: Option<Vec<InlineNode>>,
656 anchors: Option<Vec<Anchor>>,
657 level: Option<SectionLevel>,
658 metadata: Option<BlockMetadata>,
659 variant: Option<String>,
660 content: Option<serde_json::Value>,
661 notation: Option<serde_json::Value>,
662 blocks: Option<serde_json::Value>,
663 inlines: Option<Vec<InlineNode>>,
664 marker: Option<String>,
665 items: Option<serde_json::Value>,
666 location: Option<Location>,
667}
668
669fn parse_blocks<E: de::Error>(value: Option<serde_json::Value>) -> Result<Vec<Block>, E> {
671 match value {
672 Some(serde_json::Value::Array(arr)) => arr
673 .into_iter()
674 .map(|v| serde_json::from_value(v).map_err(E::custom))
675 .collect(),
676 Some(_) => Err(E::custom("blocks must be an array")),
677 None => Ok(Vec::new()),
678 }
679}
680
681fn require_blocks<E: de::Error>(value: Option<serde_json::Value>) -> Result<Vec<Block>, E> {
683 match value {
684 Some(serde_json::Value::Array(arr)) => arr
685 .into_iter()
686 .map(|v| serde_json::from_value(v).map_err(E::custom))
687 .collect(),
688 Some(_) => Err(E::custom("blocks must be an array")),
689 None => Err(E::missing_field("blocks")),
690 }
691}
692
693fn parse_list_items<E: de::Error>(value: Option<serde_json::Value>) -> Result<Vec<ListItem>, E> {
695 match value {
696 Some(serde_json::Value::Array(arr)) => arr
697 .into_iter()
698 .map(|v| serde_json::from_value(v).map_err(E::custom))
699 .collect(),
700 Some(_) => Err(E::custom("items must be an array")),
701 None => Err(E::missing_field("items")),
702 }
703}
704
705fn parse_dlist_items<E: de::Error>(
707 value: Option<serde_json::Value>,
708) -> Result<Vec<DescriptionListItem>, E> {
709 match value {
710 Some(serde_json::Value::Array(arr)) => arr
711 .into_iter()
712 .map(|v| serde_json::from_value(v).map_err(E::custom))
713 .collect(),
714 Some(_) => Err(E::custom("items must be an array")),
715 None => Err(E::missing_field("items")),
716 }
717}
718
719fn construct_section<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
724 Ok(Block::Section(Section {
725 metadata: raw.metadata.unwrap_or_default(),
726 title: raw.title.unwrap_or_default().into(),
727 level: raw.level.ok_or_else(|| E::missing_field("level"))?,
728 content: parse_blocks(raw.blocks)?,
729 location: raw.location.ok_or_else(|| E::missing_field("location"))?,
730 }))
731}
732
733fn construct_paragraph<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
734 Ok(Block::Paragraph(Paragraph {
735 metadata: raw.metadata.unwrap_or_default(),
736 title: raw.title.unwrap_or_default().into(),
737 content: raw.inlines.ok_or_else(|| E::missing_field("inlines"))?,
738 location: raw.location.ok_or_else(|| E::missing_field("location"))?,
739 }))
740}
741
742fn construct_image<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
743 let form = raw.form.ok_or_else(|| E::missing_field("form"))?;
744 if form != "macro" {
745 return Err(E::custom(format!("unexpected form: {form}")));
746 }
747 Ok(Block::Image(Image {
748 title: raw.title.unwrap_or_default().into(),
749 source: raw.source.ok_or_else(|| E::missing_field("source"))?,
750 metadata: raw.metadata.unwrap_or_default(),
751 location: raw.location.ok_or_else(|| E::missing_field("location"))?,
752 }))
753}
754
755fn construct_audio<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
756 let form = raw.form.ok_or_else(|| E::missing_field("form"))?;
757 if form != "macro" {
758 return Err(E::custom(format!("unexpected form: {form}")));
759 }
760 Ok(Block::Audio(Audio {
761 title: raw.title.unwrap_or_default().into(),
762 source: raw.source.ok_or_else(|| E::missing_field("source"))?,
763 metadata: raw.metadata.unwrap_or_default(),
764 location: raw.location.ok_or_else(|| E::missing_field("location"))?,
765 }))
766}
767
768fn construct_video<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
769 let sources = if let Some(sources_value) = raw.sources {
770 sources_value
771 } else {
772 let form = raw.form.ok_or_else(|| E::missing_field("form"))?;
774 if form != "macro" {
775 return Err(E::custom(format!("unexpected form: {form}")));
776 }
777 let target = raw.target.ok_or_else(|| E::missing_field("target"))?;
778 let source = Source::from_str(&target).map_err(E::custom)?;
779 vec![source]
780 };
781 Ok(Block::Video(Video {
782 title: raw.title.unwrap_or_default().into(),
783 sources,
784 metadata: raw.metadata.unwrap_or_default(),
785 location: raw.location.ok_or_else(|| E::missing_field("location"))?,
786 }))
787}
788
789fn construct_break<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
790 let variant = raw.variant.ok_or_else(|| E::missing_field("variant"))?;
791 let location = raw.location.ok_or_else(|| E::missing_field("location"))?;
792 match variant.as_str() {
793 "page" => Ok(Block::PageBreak(PageBreak {
794 title: raw.title.unwrap_or_default().into(),
795 metadata: raw.metadata.unwrap_or_default(),
796 location,
797 })),
798 "thematic" => Ok(Block::ThematicBreak(ThematicBreak {
799 title: raw.title.unwrap_or_default().into(),
800 anchors: raw.anchors.unwrap_or_default(),
801 location,
802 })),
803 _ => Err(E::custom(format!("unexpected 'break' variant: {variant}"))),
804 }
805}
806
807fn construct_heading<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
808 Ok(Block::DiscreteHeader(DiscreteHeader {
809 title: raw.title.unwrap_or_default().into(),
810 level: raw.level.ok_or_else(|| E::missing_field("level"))?,
811 metadata: raw.metadata.unwrap_or_default(),
812 location: raw.location.ok_or_else(|| E::missing_field("location"))?,
813 }))
814}
815
816fn construct_toc<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
817 Ok(Block::TableOfContents(TableOfContents {
818 metadata: raw.metadata.unwrap_or_default(),
819 location: raw.location.ok_or_else(|| E::missing_field("location"))?,
820 }))
821}
822
823fn construct_comment<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
824 let content = match raw.content {
825 Some(serde_json::Value::String(s)) => s,
826 Some(_) => return Err(E::custom("comment content must be a string")),
827 None => String::new(),
828 };
829 Ok(Block::Comment(Comment {
830 content,
831 location: raw.location.ok_or_else(|| E::missing_field("location"))?,
832 }))
833}
834
835fn construct_admonition<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
836 let variant = raw.variant.ok_or_else(|| E::missing_field("variant"))?;
837 Ok(Block::Admonition(Admonition {
838 metadata: raw.metadata.unwrap_or_default(),
839 variant: AdmonitionVariant::from_str(&variant).map_err(E::custom)?,
840 blocks: require_blocks(raw.blocks)?,
841 title: raw.title.unwrap_or_default().into(),
842 location: raw.location.ok_or_else(|| E::missing_field("location"))?,
843 }))
844}
845
846fn construct_dlist<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
847 Ok(Block::DescriptionList(DescriptionList {
848 title: raw.title.unwrap_or_default().into(),
849 metadata: raw.metadata.unwrap_or_default(),
850 items: parse_dlist_items(raw.items)?,
851 location: raw.location.ok_or_else(|| E::missing_field("location"))?,
852 }))
853}
854
855fn construct_list<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
856 let variant = raw.variant.ok_or_else(|| E::missing_field("variant"))?;
857 let location = raw.location.ok_or_else(|| E::missing_field("location"))?;
858 let title: Title = raw.title.unwrap_or_default().into();
859 let metadata = raw.metadata.unwrap_or_default();
860 let items = parse_list_items(raw.items)?;
861
862 match variant.as_str() {
863 "unordered" => Ok(Block::UnorderedList(UnorderedList {
864 title,
865 metadata,
866 marker: raw.marker.ok_or_else(|| E::missing_field("marker"))?,
867 items,
868 location,
869 })),
870 "ordered" => Ok(Block::OrderedList(OrderedList {
871 title,
872 metadata,
873 marker: raw.marker.ok_or_else(|| E::missing_field("marker"))?,
874 items,
875 location,
876 })),
877 "callout" => Ok(Block::CalloutList(CalloutList {
878 title,
879 metadata,
880 items,
881 location,
882 })),
883 _ => Err(E::custom(format!("unexpected 'list' variant: {variant}"))),
884 }
885}
886
887fn construct_delimited<E: de::Error>(name: &str, raw: RawBlockFields) -> Result<Block, E> {
888 let form = raw.form.ok_or_else(|| E::missing_field("form"))?;
889 if form != "delimited" {
890 return Err(E::custom(format!("unexpected form: {form}")));
891 }
892 let delimiter = raw.delimiter.ok_or_else(|| E::missing_field("delimiter"))?;
893 let location = raw.location.ok_or_else(|| E::missing_field("location"))?;
894 let metadata = raw.metadata.unwrap_or_default();
895 let title: Title = raw.title.unwrap_or_default().into();
896
897 let inner = match name {
898 "example" => DelimitedBlockType::DelimitedExample(require_blocks(raw.blocks)?),
899 "sidebar" => DelimitedBlockType::DelimitedSidebar(require_blocks(raw.blocks)?),
900 "open" => DelimitedBlockType::DelimitedOpen(require_blocks(raw.blocks)?),
901 "quote" => DelimitedBlockType::DelimitedQuote(require_blocks(raw.blocks)?),
902 "verse" => DelimitedBlockType::DelimitedVerse(
903 raw.inlines.ok_or_else(|| E::missing_field("inlines"))?,
904 ),
905 "listing" => DelimitedBlockType::DelimitedListing(
906 raw.inlines.ok_or_else(|| E::missing_field("inlines"))?,
907 ),
908 "literal" => DelimitedBlockType::DelimitedLiteral(
909 raw.inlines.ok_or_else(|| E::missing_field("inlines"))?,
910 ),
911 "pass" => DelimitedBlockType::DelimitedPass(
912 raw.inlines.ok_or_else(|| E::missing_field("inlines"))?,
913 ),
914 "stem" => {
915 let serde_json::Value::String(content) =
916 raw.content.ok_or_else(|| E::missing_field("content"))?
917 else {
918 return Err(E::custom("content must be a string"));
919 };
920 let notation = match raw.notation {
921 Some(serde_json::Value::String(n)) => {
922 StemNotation::from_str(&n).map_err(E::custom)?
923 }
924 Some(
925 serde_json::Value::Null
926 | serde_json::Value::Bool(_)
927 | serde_json::Value::Number(_)
928 | serde_json::Value::Array(_)
929 | serde_json::Value::Object(_),
930 )
931 | None => StemNotation::Latexmath,
932 };
933 DelimitedBlockType::DelimitedStem(StemContent { content, notation })
934 }
935 "table" => {
936 let table =
937 serde_json::from_value(raw.content.ok_or_else(|| E::missing_field("content"))?)
938 .map_err(|e| {
939 tracing::error!("content must be compatible with `Table` type: {e}");
940 E::custom("content must be compatible with `Table` type")
941 })?;
942 DelimitedBlockType::DelimitedTable(table)
943 }
944 _ => return Err(E::custom(format!("unexpected delimited block: {name}"))),
945 };
946
947 Ok(Block::DelimitedBlock(DelimitedBlock {
948 metadata,
949 inner,
950 delimiter,
951 title,
952 location,
953 }))
954}
955
956fn construct_document_attribute<E: de::Error>(name: &str, raw: RawBlockFields) -> Result<Block, E> {
957 let value = if let Some(value) = raw.value {
958 if value.is_empty() {
959 AttributeValue::None
960 } else if value.eq_ignore_ascii_case("true") {
961 AttributeValue::Bool(true)
962 } else if value.eq_ignore_ascii_case("false") {
963 AttributeValue::Bool(false)
964 } else {
965 AttributeValue::String(value)
966 }
967 } else {
968 AttributeValue::None
969 };
970 Ok(Block::DocumentAttribute(DocumentAttribute {
971 name: name.to_string(),
972 value,
973 location: raw.location.ok_or_else(|| E::missing_field("location"))?,
974 }))
975}
976
977fn dispatch_block<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
979 let name = raw.name.clone().ok_or_else(|| E::missing_field("name"))?;
981 let ty = raw.r#type.clone().ok_or_else(|| E::missing_field("type"))?;
982
983 match (name.as_str(), ty.as_str()) {
984 ("section", "block") => construct_section(raw),
985 ("paragraph", "block") => construct_paragraph(raw),
986 ("image", "block") => construct_image(raw),
987 ("audio", "block") => construct_audio(raw),
988 ("video", "block") => construct_video(raw),
989 ("break", "block") => construct_break(raw),
990 ("heading", "block") => construct_heading(raw),
991 ("toc", "block") => construct_toc(raw),
992 ("comment", "block") => construct_comment(raw),
993 ("admonition", "block") => construct_admonition(raw),
994 ("dlist", "block") => construct_dlist(raw),
995 ("list", "block") => construct_list(raw),
996 (
998 "example" | "sidebar" | "open" | "quote" | "verse" | "listing" | "literal" | "pass"
999 | "stem" | "table",
1000 "block",
1001 ) => construct_delimited(&name, raw),
1002 (_, "attribute") => construct_document_attribute(&name, raw),
1004 _ => Err(E::custom(format!(
1005 "unexpected name/type combination: {name}/{ty}"
1006 ))),
1007 }
1008}
1009
1010impl<'de> Deserialize<'de> for Block {
1011 fn deserialize<D>(deserializer: D) -> Result<Block, D::Error>
1012 where
1013 D: Deserializer<'de>,
1014 {
1015 let raw: RawBlockFields = RawBlockFields::deserialize(deserializer)?;
1017 dispatch_block(raw)
1018 }
1019}