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, CalloutListItem, DescriptionList, DescriptionListItem, ListItem,
29 ListItemCheckedStatus, ListLevel, 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_callout_list_items<E: de::Error>(
753 value: Option<serde_json::Value>,
754) -> Result<Vec<CalloutListItem>, 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 parse_dlist_items<E: de::Error>(
767 value: Option<serde_json::Value>,
768) -> Result<Vec<DescriptionListItem>, E> {
769 match value {
770 Some(serde_json::Value::Array(arr)) => arr
771 .into_iter()
772 .map(|v| serde_json::from_value(v).map_err(E::custom))
773 .collect(),
774 Some(_) => Err(E::custom("items must be an array")),
775 None => Err(E::missing_field("items")),
776 }
777}
778
779fn construct_section<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
784 Ok(Block::Section(Section {
785 metadata: raw.metadata.unwrap_or_default(),
786 title: raw.title.unwrap_or_default().into(),
787 level: raw.level.ok_or_else(|| E::missing_field("level"))?,
788 content: parse_blocks(raw.blocks)?,
789 location: raw.location.ok_or_else(|| E::missing_field("location"))?,
790 }))
791}
792
793fn construct_paragraph<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
794 Ok(Block::Paragraph(Paragraph {
795 metadata: raw.metadata.unwrap_or_default(),
796 title: raw.title.unwrap_or_default().into(),
797 content: raw.inlines.ok_or_else(|| E::missing_field("inlines"))?,
798 location: raw.location.ok_or_else(|| E::missing_field("location"))?,
799 }))
800}
801
802fn construct_image<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
803 let form = raw.form.ok_or_else(|| E::missing_field("form"))?;
804 if form != "macro" {
805 return Err(E::custom(format!("unexpected form: {form}")));
806 }
807 Ok(Block::Image(Image {
808 title: raw.title.unwrap_or_default().into(),
809 source: raw.source.ok_or_else(|| E::missing_field("source"))?,
810 metadata: raw.metadata.unwrap_or_default(),
811 location: raw.location.ok_or_else(|| E::missing_field("location"))?,
812 }))
813}
814
815fn construct_audio<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
816 let form = raw.form.ok_or_else(|| E::missing_field("form"))?;
817 if form != "macro" {
818 return Err(E::custom(format!("unexpected form: {form}")));
819 }
820 Ok(Block::Audio(Audio {
821 title: raw.title.unwrap_or_default().into(),
822 source: raw.source.ok_or_else(|| E::missing_field("source"))?,
823 metadata: raw.metadata.unwrap_or_default(),
824 location: raw.location.ok_or_else(|| E::missing_field("location"))?,
825 }))
826}
827
828fn construct_video<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
829 let sources = if let Some(sources_value) = raw.sources {
830 sources_value
831 } else {
832 let form = raw.form.ok_or_else(|| E::missing_field("form"))?;
834 if form != "macro" {
835 return Err(E::custom(format!("unexpected form: {form}")));
836 }
837 let target = raw.target.ok_or_else(|| E::missing_field("target"))?;
838 let source = Source::from_str(&target).map_err(E::custom)?;
839 vec![source]
840 };
841 Ok(Block::Video(Video {
842 title: raw.title.unwrap_or_default().into(),
843 sources,
844 metadata: raw.metadata.unwrap_or_default(),
845 location: raw.location.ok_or_else(|| E::missing_field("location"))?,
846 }))
847}
848
849fn construct_break<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
850 let variant = raw.variant.ok_or_else(|| E::missing_field("variant"))?;
851 let location = raw.location.ok_or_else(|| E::missing_field("location"))?;
852 match variant.as_str() {
853 "page" => Ok(Block::PageBreak(PageBreak {
854 title: raw.title.unwrap_or_default().into(),
855 metadata: raw.metadata.unwrap_or_default(),
856 location,
857 })),
858 "thematic" => Ok(Block::ThematicBreak(ThematicBreak {
859 title: raw.title.unwrap_or_default().into(),
860 anchors: raw.anchors.unwrap_or_default(),
861 location,
862 })),
863 _ => Err(E::custom(format!("unexpected 'break' variant: {variant}"))),
864 }
865}
866
867fn construct_heading<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
868 Ok(Block::DiscreteHeader(DiscreteHeader {
869 title: raw.title.unwrap_or_default().into(),
870 level: raw.level.ok_or_else(|| E::missing_field("level"))?,
871 metadata: raw.metadata.unwrap_or_default(),
872 location: raw.location.ok_or_else(|| E::missing_field("location"))?,
873 }))
874}
875
876fn construct_toc<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
877 Ok(Block::TableOfContents(TableOfContents {
878 metadata: raw.metadata.unwrap_or_default(),
879 location: raw.location.ok_or_else(|| E::missing_field("location"))?,
880 }))
881}
882
883fn construct_comment<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
884 let content = match raw.content {
885 Some(serde_json::Value::String(s)) => s,
886 Some(_) => return Err(E::custom("comment content must be a string")),
887 None => String::new(),
888 };
889 Ok(Block::Comment(Comment {
890 content,
891 location: raw.location.ok_or_else(|| E::missing_field("location"))?,
892 }))
893}
894
895fn construct_admonition<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
896 let variant = raw.variant.ok_or_else(|| E::missing_field("variant"))?;
897 Ok(Block::Admonition(Admonition {
898 metadata: raw.metadata.unwrap_or_default(),
899 variant: AdmonitionVariant::from_str(&variant).map_err(E::custom)?,
900 blocks: require_blocks(raw.blocks)?,
901 title: raw.title.unwrap_or_default().into(),
902 location: raw.location.ok_or_else(|| E::missing_field("location"))?,
903 }))
904}
905
906fn construct_dlist<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
907 Ok(Block::DescriptionList(DescriptionList {
908 title: raw.title.unwrap_or_default().into(),
909 metadata: raw.metadata.unwrap_or_default(),
910 items: parse_dlist_items(raw.items)?,
911 location: raw.location.ok_or_else(|| E::missing_field("location"))?,
912 }))
913}
914
915fn construct_list<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
916 let variant = raw.variant.ok_or_else(|| E::missing_field("variant"))?;
917 let location = raw.location.ok_or_else(|| E::missing_field("location"))?;
918 let title: Title = raw.title.unwrap_or_default().into();
919 let metadata = raw.metadata.unwrap_or_default();
920
921 match variant.as_str() {
922 "unordered" => {
923 let items = parse_list_items(raw.items)?;
924 Ok(Block::UnorderedList(UnorderedList {
925 title,
926 metadata,
927 marker: raw.marker.ok_or_else(|| E::missing_field("marker"))?,
928 items,
929 location,
930 }))
931 }
932 "ordered" => {
933 let items = parse_list_items(raw.items)?;
934 Ok(Block::OrderedList(OrderedList {
935 title,
936 metadata,
937 marker: raw.marker.ok_or_else(|| E::missing_field("marker"))?,
938 items,
939 location,
940 }))
941 }
942 "callout" => {
943 let items = parse_callout_list_items(raw.items)?;
944 Ok(Block::CalloutList(CalloutList {
945 title,
946 metadata,
947 items,
948 location,
949 }))
950 }
951 _ => Err(E::custom(format!("unexpected 'list' variant: {variant}"))),
952 }
953}
954
955fn construct_delimited<E: de::Error>(name: &str, raw: RawBlockFields) -> Result<Block, E> {
956 let form = raw.form.ok_or_else(|| E::missing_field("form"))?;
957 if form != "delimited" {
958 return Err(E::custom(format!("unexpected form: {form}")));
959 }
960 let delimiter = raw.delimiter.ok_or_else(|| E::missing_field("delimiter"))?;
961 let location = raw.location.ok_or_else(|| E::missing_field("location"))?;
962 let metadata = raw.metadata.unwrap_or_default();
963 let title: Title = raw.title.unwrap_or_default().into();
964
965 let inner = match name {
966 "example" => DelimitedBlockType::DelimitedExample(require_blocks(raw.blocks)?),
967 "sidebar" => DelimitedBlockType::DelimitedSidebar(require_blocks(raw.blocks)?),
968 "open" => DelimitedBlockType::DelimitedOpen(require_blocks(raw.blocks)?),
969 "quote" => DelimitedBlockType::DelimitedQuote(require_blocks(raw.blocks)?),
970 "verse" => DelimitedBlockType::DelimitedVerse(
971 raw.inlines.ok_or_else(|| E::missing_field("inlines"))?,
972 ),
973 "listing" => DelimitedBlockType::DelimitedListing(
974 raw.inlines.ok_or_else(|| E::missing_field("inlines"))?,
975 ),
976 "literal" => DelimitedBlockType::DelimitedLiteral(
977 raw.inlines.ok_or_else(|| E::missing_field("inlines"))?,
978 ),
979 "pass" => DelimitedBlockType::DelimitedPass(
980 raw.inlines.ok_or_else(|| E::missing_field("inlines"))?,
981 ),
982 "stem" => {
983 let serde_json::Value::String(content) =
984 raw.content.ok_or_else(|| E::missing_field("content"))?
985 else {
986 return Err(E::custom("content must be a string"));
987 };
988 let notation = match raw.notation {
989 Some(serde_json::Value::String(n)) => {
990 StemNotation::from_str(&n).map_err(E::custom)?
991 }
992 Some(
993 serde_json::Value::Null
994 | serde_json::Value::Bool(_)
995 | serde_json::Value::Number(_)
996 | serde_json::Value::Array(_)
997 | serde_json::Value::Object(_),
998 )
999 | None => StemNotation::Latexmath,
1000 };
1001 DelimitedBlockType::DelimitedStem(StemContent { content, notation })
1002 }
1003 "table" => {
1004 let table =
1005 serde_json::from_value(raw.content.ok_or_else(|| E::missing_field("content"))?)
1006 .map_err(|e| {
1007 tracing::error!("content must be compatible with `Table` type: {e}");
1008 E::custom("content must be compatible with `Table` type")
1009 })?;
1010 DelimitedBlockType::DelimitedTable(table)
1011 }
1012 _ => return Err(E::custom(format!("unexpected delimited block: {name}"))),
1013 };
1014
1015 Ok(Block::DelimitedBlock(DelimitedBlock {
1016 metadata,
1017 inner,
1018 delimiter,
1019 title,
1020 location,
1021 }))
1022}
1023
1024fn construct_document_attribute<E: de::Error>(name: &str, raw: RawBlockFields) -> Result<Block, E> {
1025 let value = if let Some(value) = raw.value {
1026 if value.is_empty() {
1027 AttributeValue::None
1028 } else if value.eq_ignore_ascii_case("true") {
1029 AttributeValue::Bool(true)
1030 } else if value.eq_ignore_ascii_case("false") {
1031 AttributeValue::Bool(false)
1032 } else {
1033 AttributeValue::String(value)
1034 }
1035 } else {
1036 AttributeValue::None
1037 };
1038 Ok(Block::DocumentAttribute(DocumentAttribute {
1039 name: name.to_string(),
1040 value,
1041 location: raw.location.ok_or_else(|| E::missing_field("location"))?,
1042 }))
1043}
1044
1045fn dispatch_block<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
1047 let name = raw.name.clone().ok_or_else(|| E::missing_field("name"))?;
1049 let ty = raw.r#type.clone().ok_or_else(|| E::missing_field("type"))?;
1050
1051 match (name.as_str(), ty.as_str()) {
1052 ("section", "block") => construct_section(raw),
1053 ("paragraph", "block") => construct_paragraph(raw),
1054 ("image", "block") => construct_image(raw),
1055 ("audio", "block") => construct_audio(raw),
1056 ("video", "block") => construct_video(raw),
1057 ("break", "block") => construct_break(raw),
1058 ("heading", "block") => construct_heading(raw),
1059 ("toc", "block") => construct_toc(raw),
1060 ("comment", "block") => construct_comment(raw),
1061 ("admonition", "block") => construct_admonition(raw),
1062 ("dlist", "block") => construct_dlist(raw),
1063 ("list", "block") => construct_list(raw),
1064 (
1066 "example" | "sidebar" | "open" | "quote" | "verse" | "listing" | "literal" | "pass"
1067 | "stem" | "table",
1068 "block",
1069 ) => construct_delimited(&name, raw),
1070 (_, "attribute") => construct_document_attribute(&name, raw),
1072 _ => Err(E::custom(format!(
1073 "unexpected name/type combination: {name}/{ty}"
1074 ))),
1075 }
1076}
1077
1078impl<'de> Deserialize<'de> for Block {
1079 fn deserialize<D>(deserializer: D) -> Result<Block, D::Error>
1080 where
1081 D: Deserializer<'de>,
1082 {
1083 let raw: RawBlockFields = RawBlockFields::deserialize(deserializer)?;
1085 dispatch_block(raw)
1086 }
1087}