1use serde::{Deserialize, Serialize};
4
5use crate::{Manifest, McdPackage, markdown};
6
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9pub struct McdDocument {
10 pub source_path: String,
12 pub blocks: Vec<DocumentBlock>,
14}
15
16impl McdDocument {
17 pub fn from_package(package: &McdPackage, manifest: &Manifest) -> crate::Result<Self> {
19 let markdown = package.read_to_string(&manifest.entrypoint)?;
20 markdown::parse_markdown(&manifest.entrypoint, &markdown)
21 }
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(tag = "type", rename_all = "snake_case")]
27pub enum DocumentBlock {
28 Heading {
30 id: String,
32 level: u8,
34 text: String,
36 #[serde(skip_serializing_if = "Option::is_none")]
38 source: Option<SourceSpan>,
39 #[serde(default, skip_serializing_if = "Vec::is_empty")]
41 annotations: Vec<AnnotationRef>,
42 },
43 Paragraph {
45 id: String,
47 text: String,
49 #[serde(skip_serializing_if = "Option::is_none")]
51 source: Option<SourceSpan>,
52 #[serde(default, skip_serializing_if = "Vec::is_empty")]
54 annotations: Vec<AnnotationRef>,
55 },
56 List {
58 id: String,
60 text: String,
62 #[serde(skip_serializing_if = "Option::is_none")]
64 source: Option<SourceSpan>,
65 #[serde(default, skip_serializing_if = "Vec::is_empty")]
67 annotations: Vec<AnnotationRef>,
68 },
69 CodeBlock {
71 id: String,
73 #[serde(skip_serializing_if = "Option::is_none")]
75 language: Option<String>,
76 text: String,
78 #[serde(skip_serializing_if = "Option::is_none")]
80 source: Option<SourceSpan>,
81 #[serde(default, skip_serializing_if = "Vec::is_empty")]
83 annotations: Vec<AnnotationRef>,
84 },
85 Quote {
87 id: String,
89 text: String,
91 #[serde(skip_serializing_if = "Option::is_none")]
93 source: Option<SourceSpan>,
94 #[serde(default, skip_serializing_if = "Vec::is_empty")]
96 annotations: Vec<AnnotationRef>,
97 },
98 MathBlock {
100 id: String,
102 text: String,
104 #[serde(skip_serializing_if = "Option::is_none")]
106 source: Option<SourceSpan>,
107 },
108 TableRef {
110 id: String,
112 placement: crate::directives::TablePlacement,
114 #[serde(skip_serializing_if = "Option::is_none")]
116 source: Option<SourceSpan>,
117 },
118 ImageRef {
120 id: String,
122 placement: crate::directives::ImagePlacement,
124 #[serde(skip_serializing_if = "Option::is_none")]
126 source: Option<SourceSpan>,
127 },
128}
129
130impl DocumentBlock {
131 #[must_use]
133 pub fn id(&self) -> &str {
134 match self {
135 Self::Heading { id, .. }
136 | Self::Paragraph { id, .. }
137 | Self::List { id, .. }
138 | Self::CodeBlock { id, .. }
139 | Self::Quote { id, .. }
140 | Self::MathBlock { id, .. }
141 | Self::TableRef { id, .. }
142 | Self::ImageRef { id, .. } => id,
143 }
144 }
145
146 #[must_use]
148 pub fn annotation_refs(&self) -> &[AnnotationRef] {
149 match self {
150 Self::Heading { annotations, .. }
151 | Self::Paragraph { annotations, .. }
152 | Self::List { annotations, .. }
153 | Self::Quote { annotations, .. } => annotations,
154 Self::TableRef { placement, .. } => &placement.annotations,
155 Self::ImageRef { placement, .. } => &placement.annotations,
156 Self::CodeBlock { .. } | Self::MathBlock { .. } => &[],
157 }
158 }
159}
160
161#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
163#[serde(rename_all = "camelCase")]
164pub struct AnnotationRef {
165 pub id: String,
167 #[serde(default, skip_serializing_if = "Option::is_none")]
169 pub text_offset: Option<usize>,
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
174#[serde(rename_all = "camelCase")]
175pub struct SourceSpan {
176 pub start_line: usize,
178 pub start_column: usize,
180 pub end_line: usize,
182 pub end_column: usize,
184}
185
186impl std::fmt::Display for SourceSpan {
187 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188 write!(
189 f,
190 "{}:{}-{}:{}",
191 self.start_line, self.start_column, self.end_line, self.end_column
192 )
193 }
194}