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]
529#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
530#[serde(untagged)]
531pub enum DelimitedBlockType {
532 DelimitedComment(Vec<InlineNode>),
534 DelimitedExample(Vec<Block>),
536 DelimitedListing(Vec<InlineNode>),
538 DelimitedLiteral(Vec<InlineNode>),
540 DelimitedOpen(Vec<Block>),
542 DelimitedSidebar(Vec<Block>),
544 DelimitedTable(Table),
546 DelimitedPass(Vec<InlineNode>),
548 DelimitedQuote(Vec<Block>),
550 DelimitedVerse(Vec<InlineNode>),
552 DelimitedStem(StemContent),
554}
555
556impl DelimitedBlockType {
557 fn name(&self) -> &'static str {
558 match self {
559 DelimitedBlockType::DelimitedComment(_) => "comment",
560 DelimitedBlockType::DelimitedExample(_) => "example",
561 DelimitedBlockType::DelimitedListing(_) => "listing",
562 DelimitedBlockType::DelimitedLiteral(_) => "literal",
563 DelimitedBlockType::DelimitedOpen(_) => "open",
564 DelimitedBlockType::DelimitedSidebar(_) => "sidebar",
565 DelimitedBlockType::DelimitedTable(_) => "table",
566 DelimitedBlockType::DelimitedPass(_) => "pass",
567 DelimitedBlockType::DelimitedQuote(_) => "quote",
568 DelimitedBlockType::DelimitedVerse(_) => "verse",
569 DelimitedBlockType::DelimitedStem(_) => "stem",
570 }
571 }
572}
573
574impl Serialize for Document {
575 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
576 where
577 S: Serializer,
578 {
579 let mut state = serializer.serialize_map(None)?;
580 state.serialize_entry("name", "document")?;
581 state.serialize_entry("type", "block")?;
582 if let Some(header) = &self.header {
583 state.serialize_entry("header", header)?;
584 state.serialize_entry("attributes", &self.attributes)?;
587 } else if !self.attributes.is_empty() {
588 state.serialize_entry("attributes", &self.attributes)?;
589 }
590 if !self.blocks.is_empty() {
591 state.serialize_entry("blocks", &self.blocks)?;
592 }
593 state.serialize_entry("location", &self.location)?;
594 state.end()
595 }
596}
597
598impl Serialize for DelimitedBlock {
599 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
600 where
601 S: Serializer,
602 {
603 let mut state = serializer.serialize_map(None)?;
604 state.serialize_entry("name", self.inner.name())?;
605 state.serialize_entry("type", "block")?;
606 state.serialize_entry("form", "delimited")?;
607 state.serialize_entry("delimiter", &self.delimiter)?;
608 if !self.metadata.is_default() {
609 state.serialize_entry("metadata", &self.metadata)?;
610 }
611
612 match &self.inner {
613 DelimitedBlockType::DelimitedStem(stem) => {
614 state.serialize_entry("content", &stem.content)?;
615 state.serialize_entry("notation", &stem.notation)?;
616 }
617 DelimitedBlockType::DelimitedListing(inner)
618 | DelimitedBlockType::DelimitedLiteral(inner)
619 | DelimitedBlockType::DelimitedPass(inner)
620 | DelimitedBlockType::DelimitedVerse(inner) => {
621 state.serialize_entry("inlines", &inner)?;
622 }
623 DelimitedBlockType::DelimitedTable(inner) => {
624 state.serialize_entry("content", &inner)?;
625 }
626 inner @ (DelimitedBlockType::DelimitedComment(_)
627 | DelimitedBlockType::DelimitedExample(_)
628 | DelimitedBlockType::DelimitedOpen(_)
629 | DelimitedBlockType::DelimitedQuote(_)
630 | DelimitedBlockType::DelimitedSidebar(_)) => {
631 state.serialize_entry("blocks", &inner)?;
632 }
633 }
634 if !self.title.is_empty() {
635 state.serialize_entry("title", &self.title)?;
636 }
637 state.serialize_entry("location", &self.location)?;
638 state.end()
639 }
640}
641
642impl Serialize for DiscreteHeader {
643 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
644 where
645 S: Serializer,
646 {
647 let mut state = serializer.serialize_map(None)?;
648 state.serialize_entry("name", "heading")?;
649 state.serialize_entry("type", "block")?;
650 if !self.title.is_empty() {
651 state.serialize_entry("title", &self.title)?;
652 }
653 state.serialize_entry("level", &self.level)?;
654 if !self.metadata.is_default() {
655 state.serialize_entry("metadata", &self.metadata)?;
656 }
657 state.serialize_entry("location", &self.location)?;
658 state.end()
659 }
660}
661
662impl Serialize for Paragraph {
663 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
664 where
665 S: Serializer,
666 {
667 let mut state = serializer.serialize_map(None)?;
668 state.serialize_entry("name", "paragraph")?;
669 state.serialize_entry("type", "block")?;
670 if !self.title.is_empty() {
671 state.serialize_entry("title", &self.title)?;
672 }
673 state.serialize_entry("inlines", &self.content)?;
674 if !self.metadata.is_default() {
675 state.serialize_entry("metadata", &self.metadata)?;
676 }
677 state.serialize_entry("location", &self.location)?;
678 state.end()
679 }
680}
681
682#[derive(Default, Deserialize)]
689#[serde(default)]
690struct RawBlockFields {
691 name: Option<String>,
692 r#type: Option<String>,
693 value: Option<String>,
694 form: Option<String>,
695 target: Option<String>,
696 source: Option<Source>,
697 sources: Option<Vec<Source>>,
698 delimiter: Option<String>,
699 reftext: Option<String>,
700 id: Option<String>,
701 title: Option<Vec<InlineNode>>,
702 anchors: Option<Vec<Anchor>>,
703 level: Option<SectionLevel>,
704 metadata: Option<BlockMetadata>,
705 variant: Option<String>,
706 content: Option<serde_json::Value>,
707 notation: Option<serde_json::Value>,
708 blocks: Option<serde_json::Value>,
709 inlines: Option<Vec<InlineNode>>,
710 marker: Option<String>,
711 items: Option<serde_json::Value>,
712 location: Option<Location>,
713}
714
715fn parse_blocks<E: de::Error>(value: Option<serde_json::Value>) -> Result<Vec<Block>, E> {
717 match value {
718 Some(serde_json::Value::Array(arr)) => arr
719 .into_iter()
720 .map(|v| serde_json::from_value(v).map_err(E::custom))
721 .collect(),
722 Some(_) => Err(E::custom("blocks must be an array")),
723 None => Ok(Vec::new()),
724 }
725}
726
727fn require_blocks<E: de::Error>(value: Option<serde_json::Value>) -> Result<Vec<Block>, E> {
729 match value {
730 Some(serde_json::Value::Array(arr)) => arr
731 .into_iter()
732 .map(|v| serde_json::from_value(v).map_err(E::custom))
733 .collect(),
734 Some(_) => Err(E::custom("blocks must be an array")),
735 None => Err(E::missing_field("blocks")),
736 }
737}
738
739fn parse_list_items<E: de::Error>(value: Option<serde_json::Value>) -> Result<Vec<ListItem>, E> {
741 match value {
742 Some(serde_json::Value::Array(arr)) => arr
743 .into_iter()
744 .map(|v| serde_json::from_value(v).map_err(E::custom))
745 .collect(),
746 Some(_) => Err(E::custom("items must be an array")),
747 None => Err(E::missing_field("items")),
748 }
749}
750
751fn parse_dlist_items<E: de::Error>(
753 value: Option<serde_json::Value>,
754) -> Result<Vec<DescriptionListItem>, E> {
755 match value {
756 Some(serde_json::Value::Array(arr)) => arr
757 .into_iter()
758 .map(|v| serde_json::from_value(v).map_err(E::custom))
759 .collect(),
760 Some(_) => Err(E::custom("items must be an array")),
761 None => Err(E::missing_field("items")),
762 }
763}
764
765fn construct_section<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
770 Ok(Block::Section(Section {
771 metadata: raw.metadata.unwrap_or_default(),
772 title: raw.title.unwrap_or_default().into(),
773 level: raw.level.ok_or_else(|| E::missing_field("level"))?,
774 content: parse_blocks(raw.blocks)?,
775 location: raw.location.ok_or_else(|| E::missing_field("location"))?,
776 }))
777}
778
779fn construct_paragraph<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
780 Ok(Block::Paragraph(Paragraph {
781 metadata: raw.metadata.unwrap_or_default(),
782 title: raw.title.unwrap_or_default().into(),
783 content: raw.inlines.ok_or_else(|| E::missing_field("inlines"))?,
784 location: raw.location.ok_or_else(|| E::missing_field("location"))?,
785 }))
786}
787
788fn construct_image<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
789 let form = raw.form.ok_or_else(|| E::missing_field("form"))?;
790 if form != "macro" {
791 return Err(E::custom(format!("unexpected form: {form}")));
792 }
793 Ok(Block::Image(Image {
794 title: raw.title.unwrap_or_default().into(),
795 source: raw.source.ok_or_else(|| E::missing_field("source"))?,
796 metadata: raw.metadata.unwrap_or_default(),
797 location: raw.location.ok_or_else(|| E::missing_field("location"))?,
798 }))
799}
800
801fn construct_audio<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
802 let form = raw.form.ok_or_else(|| E::missing_field("form"))?;
803 if form != "macro" {
804 return Err(E::custom(format!("unexpected form: {form}")));
805 }
806 Ok(Block::Audio(Audio {
807 title: raw.title.unwrap_or_default().into(),
808 source: raw.source.ok_or_else(|| E::missing_field("source"))?,
809 metadata: raw.metadata.unwrap_or_default(),
810 location: raw.location.ok_or_else(|| E::missing_field("location"))?,
811 }))
812}
813
814fn construct_video<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
815 let sources = if let Some(sources_value) = raw.sources {
816 sources_value
817 } else {
818 let form = raw.form.ok_or_else(|| E::missing_field("form"))?;
820 if form != "macro" {
821 return Err(E::custom(format!("unexpected form: {form}")));
822 }
823 let target = raw.target.ok_or_else(|| E::missing_field("target"))?;
824 let source = Source::from_str(&target).map_err(E::custom)?;
825 vec![source]
826 };
827 Ok(Block::Video(Video {
828 title: raw.title.unwrap_or_default().into(),
829 sources,
830 metadata: raw.metadata.unwrap_or_default(),
831 location: raw.location.ok_or_else(|| E::missing_field("location"))?,
832 }))
833}
834
835fn construct_break<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
836 let variant = raw.variant.ok_or_else(|| E::missing_field("variant"))?;
837 let location = raw.location.ok_or_else(|| E::missing_field("location"))?;
838 match variant.as_str() {
839 "page" => Ok(Block::PageBreak(PageBreak {
840 title: raw.title.unwrap_or_default().into(),
841 metadata: raw.metadata.unwrap_or_default(),
842 location,
843 })),
844 "thematic" => Ok(Block::ThematicBreak(ThematicBreak {
845 title: raw.title.unwrap_or_default().into(),
846 anchors: raw.anchors.unwrap_or_default(),
847 location,
848 })),
849 _ => Err(E::custom(format!("unexpected 'break' variant: {variant}"))),
850 }
851}
852
853fn construct_heading<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
854 Ok(Block::DiscreteHeader(DiscreteHeader {
855 title: raw.title.unwrap_or_default().into(),
856 level: raw.level.ok_or_else(|| E::missing_field("level"))?,
857 metadata: raw.metadata.unwrap_or_default(),
858 location: raw.location.ok_or_else(|| E::missing_field("location"))?,
859 }))
860}
861
862fn construct_toc<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
863 Ok(Block::TableOfContents(TableOfContents {
864 metadata: raw.metadata.unwrap_or_default(),
865 location: raw.location.ok_or_else(|| E::missing_field("location"))?,
866 }))
867}
868
869fn construct_comment<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
870 let content = match raw.content {
871 Some(serde_json::Value::String(s)) => s,
872 Some(_) => return Err(E::custom("comment content must be a string")),
873 None => String::new(),
874 };
875 Ok(Block::Comment(Comment {
876 content,
877 location: raw.location.ok_or_else(|| E::missing_field("location"))?,
878 }))
879}
880
881fn construct_admonition<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
882 let variant = raw.variant.ok_or_else(|| E::missing_field("variant"))?;
883 Ok(Block::Admonition(Admonition {
884 metadata: raw.metadata.unwrap_or_default(),
885 variant: AdmonitionVariant::from_str(&variant).map_err(E::custom)?,
886 blocks: require_blocks(raw.blocks)?,
887 title: raw.title.unwrap_or_default().into(),
888 location: raw.location.ok_or_else(|| E::missing_field("location"))?,
889 }))
890}
891
892fn construct_dlist<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
893 Ok(Block::DescriptionList(DescriptionList {
894 title: raw.title.unwrap_or_default().into(),
895 metadata: raw.metadata.unwrap_or_default(),
896 items: parse_dlist_items(raw.items)?,
897 location: raw.location.ok_or_else(|| E::missing_field("location"))?,
898 }))
899}
900
901fn construct_list<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
902 let variant = raw.variant.ok_or_else(|| E::missing_field("variant"))?;
903 let location = raw.location.ok_or_else(|| E::missing_field("location"))?;
904 let title: Title = raw.title.unwrap_or_default().into();
905 let metadata = raw.metadata.unwrap_or_default();
906 let items = parse_list_items(raw.items)?;
907
908 match variant.as_str() {
909 "unordered" => Ok(Block::UnorderedList(UnorderedList {
910 title,
911 metadata,
912 marker: raw.marker.ok_or_else(|| E::missing_field("marker"))?,
913 items,
914 location,
915 })),
916 "ordered" => Ok(Block::OrderedList(OrderedList {
917 title,
918 metadata,
919 marker: raw.marker.ok_or_else(|| E::missing_field("marker"))?,
920 items,
921 location,
922 })),
923 "callout" => Ok(Block::CalloutList(CalloutList {
924 title,
925 metadata,
926 items,
927 location,
928 })),
929 _ => Err(E::custom(format!("unexpected 'list' variant: {variant}"))),
930 }
931}
932
933fn construct_delimited<E: de::Error>(name: &str, raw: RawBlockFields) -> Result<Block, E> {
934 let form = raw.form.ok_or_else(|| E::missing_field("form"))?;
935 if form != "delimited" {
936 return Err(E::custom(format!("unexpected form: {form}")));
937 }
938 let delimiter = raw.delimiter.ok_or_else(|| E::missing_field("delimiter"))?;
939 let location = raw.location.ok_or_else(|| E::missing_field("location"))?;
940 let metadata = raw.metadata.unwrap_or_default();
941 let title: Title = raw.title.unwrap_or_default().into();
942
943 let inner = match name {
944 "example" => DelimitedBlockType::DelimitedExample(require_blocks(raw.blocks)?),
945 "sidebar" => DelimitedBlockType::DelimitedSidebar(require_blocks(raw.blocks)?),
946 "open" => DelimitedBlockType::DelimitedOpen(require_blocks(raw.blocks)?),
947 "quote" => DelimitedBlockType::DelimitedQuote(require_blocks(raw.blocks)?),
948 "verse" => DelimitedBlockType::DelimitedVerse(
949 raw.inlines.ok_or_else(|| E::missing_field("inlines"))?,
950 ),
951 "listing" => DelimitedBlockType::DelimitedListing(
952 raw.inlines.ok_or_else(|| E::missing_field("inlines"))?,
953 ),
954 "literal" => DelimitedBlockType::DelimitedLiteral(
955 raw.inlines.ok_or_else(|| E::missing_field("inlines"))?,
956 ),
957 "pass" => DelimitedBlockType::DelimitedPass(
958 raw.inlines.ok_or_else(|| E::missing_field("inlines"))?,
959 ),
960 "stem" => {
961 let serde_json::Value::String(content) =
962 raw.content.ok_or_else(|| E::missing_field("content"))?
963 else {
964 return Err(E::custom("content must be a string"));
965 };
966 let notation = match raw.notation {
967 Some(serde_json::Value::String(n)) => {
968 StemNotation::from_str(&n).map_err(E::custom)?
969 }
970 Some(
971 serde_json::Value::Null
972 | serde_json::Value::Bool(_)
973 | serde_json::Value::Number(_)
974 | serde_json::Value::Array(_)
975 | serde_json::Value::Object(_),
976 )
977 | None => StemNotation::Latexmath,
978 };
979 DelimitedBlockType::DelimitedStem(StemContent { content, notation })
980 }
981 "table" => {
982 let table =
983 serde_json::from_value(raw.content.ok_or_else(|| E::missing_field("content"))?)
984 .map_err(|e| {
985 tracing::error!("content must be compatible with `Table` type: {e}");
986 E::custom("content must be compatible with `Table` type")
987 })?;
988 DelimitedBlockType::DelimitedTable(table)
989 }
990 _ => return Err(E::custom(format!("unexpected delimited block: {name}"))),
991 };
992
993 Ok(Block::DelimitedBlock(DelimitedBlock {
994 metadata,
995 inner,
996 delimiter,
997 title,
998 location,
999 }))
1000}
1001
1002fn construct_document_attribute<E: de::Error>(name: &str, raw: RawBlockFields) -> Result<Block, E> {
1003 let value = if let Some(value) = raw.value {
1004 if value.is_empty() {
1005 AttributeValue::None
1006 } else if value.eq_ignore_ascii_case("true") {
1007 AttributeValue::Bool(true)
1008 } else if value.eq_ignore_ascii_case("false") {
1009 AttributeValue::Bool(false)
1010 } else {
1011 AttributeValue::String(value)
1012 }
1013 } else {
1014 AttributeValue::None
1015 };
1016 Ok(Block::DocumentAttribute(DocumentAttribute {
1017 name: name.to_string(),
1018 value,
1019 location: raw.location.ok_or_else(|| E::missing_field("location"))?,
1020 }))
1021}
1022
1023fn dispatch_block<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
1025 let name = raw.name.clone().ok_or_else(|| E::missing_field("name"))?;
1027 let ty = raw.r#type.clone().ok_or_else(|| E::missing_field("type"))?;
1028
1029 match (name.as_str(), ty.as_str()) {
1030 ("section", "block") => construct_section(raw),
1031 ("paragraph", "block") => construct_paragraph(raw),
1032 ("image", "block") => construct_image(raw),
1033 ("audio", "block") => construct_audio(raw),
1034 ("video", "block") => construct_video(raw),
1035 ("break", "block") => construct_break(raw),
1036 ("heading", "block") => construct_heading(raw),
1037 ("toc", "block") => construct_toc(raw),
1038 ("comment", "block") => construct_comment(raw),
1039 ("admonition", "block") => construct_admonition(raw),
1040 ("dlist", "block") => construct_dlist(raw),
1041 ("list", "block") => construct_list(raw),
1042 (
1044 "example" | "sidebar" | "open" | "quote" | "verse" | "listing" | "literal" | "pass"
1045 | "stem" | "table",
1046 "block",
1047 ) => construct_delimited(&name, raw),
1048 (_, "attribute") => construct_document_attribute(&name, raw),
1050 _ => Err(E::custom(format!(
1051 "unexpected name/type combination: {name}/{ty}"
1052 ))),
1053 }
1054}
1055
1056impl<'de> Deserialize<'de> for Block {
1057 fn deserialize<D>(deserializer: D) -> Result<Block, D::Error>
1058 where
1059 D: Deserializer<'de>,
1060 {
1061 let raw: RawBlockFields = RawBlockFields::deserialize(deserializer)?;
1063 dispatch_block(raw)
1064 }
1065}