ephemeris_documents/markdown/
ast.rs1use facet::Facet;
4
5use super::marker_name::MarkerNameBuf;
6use crate::error::MarkdownError;
7
8#[derive(Debug, Clone, Copy, Facet, PartialEq, Eq)]
10#[facet(proxy = u8)]
11pub struct HeadingLevel(u8);
12
13impl HeadingLevel {
14 pub const H1: Self = Self(1);
16 pub const H2: Self = Self(2);
18 pub const H3: Self = Self(3);
20 pub const H4: Self = Self(4);
22 pub const H5: Self = Self(5);
24 pub const H6: Self = Self(6);
26
27 #[must_use]
29 pub const fn new(level: u8) -> Option<Self> {
30 if matches!(level, 1..=6) {
31 Some(Self(level))
32 } else {
33 None
34 }
35 }
36
37 #[must_use]
39 pub const fn get(self) -> u8 {
40 self.0
41 }
42}
43
44impl TryFrom<u8> for HeadingLevel {
45 type Error = MarkdownError;
46
47 fn try_from(level: u8) -> Result<Self, Self::Error> {
48 Self::new(level).ok_or(MarkdownError::HeadingLevelOutOfRange { level })
49 }
50}
51
52impl From<&HeadingLevel> for u8 {
53 fn from(level: &HeadingLevel) -> Self {
54 level.0
55 }
56}
57
58#[derive(Debug, Clone, Facet, PartialEq, Eq)]
60#[facet(tag = "type", deny_unknown_fields)]
61#[repr(u8)]
62pub enum MarkdownInline {
63 #[facet(rename = "text")]
65 Text {
66 value: String,
68 },
69 #[facet(rename = "emphasis")]
71 Emphasis {
72 content: Vec<Self>,
74 },
75 #[facet(rename = "strong")]
77 Strong {
78 content: Vec<Self>,
80 },
81 #[facet(rename = "code")]
83 Code {
84 value: String,
86 },
87 #[facet(rename = "link")]
89 Link {
90 destination: String,
92 content: Vec<Self>,
94 },
95}
96
97#[derive(Debug, Clone, Facet, PartialEq, Eq)]
99#[facet(tag = "type", deny_unknown_fields)]
100#[repr(u8)]
101pub enum MarkdownBlock {
102 #[facet(rename = "heading")]
104 Heading {
105 level: HeadingLevel,
107 content: Vec<MarkdownInline>,
109 },
110 #[facet(rename = "paragraph")]
112 Paragraph {
113 content: Vec<MarkdownInline>,
115 },
116 #[facet(rename = "unordered-list")]
118 UnorderedList {
119 items: Vec<Vec<Self>>,
121 },
122 #[facet(rename = "code-block")]
124 CodeBlock {
125 language: Option<String>,
127 value: String,
129 },
130}
131
132impl MarkdownBlock {
133 pub(crate) fn contains_heading(&self, level: HeadingLevel) -> bool {
135 match self {
136 Self::Heading {
137 level: own,
138 content: _,
139 } => *own == level,
140 Self::Paragraph { content: _ }
141 | Self::CodeBlock {
142 language: _,
143 value: _,
144 } => false,
145 Self::UnorderedList { items } => items
146 .iter()
147 .any(|item| item.iter().any(|block| block.contains_heading(level))),
148 }
149 }
150}
151
152#[derive(Debug, Clone, Facet, PartialEq, Eq)]
154#[facet(tag = "type", deny_unknown_fields)]
155#[repr(u8)]
156pub enum MarkdownSelector {
157 #[facet(rename = "heading")]
159 Heading {
160 level: HeadingLevel,
162 content: String,
164 },
165 #[facet(rename = "marker")]
167 Marker {
168 name: MarkerNameBuf,
170 blocks: Vec<MarkdownBlock>,
172 },
173}
174
175#[cfg(test)]
176mod tests {
177 use facet_value::Value;
178
179 use super::{HeadingLevel, MarkdownBlock, MarkdownSelector};
180
181 const GOOD_LEVEL: &str = r#"{"type":"heading","level":2,"content":[]}"#;
182 const BAD_LEVEL: &str = r#"{"type":"heading","level":7,"content":[]}"#;
183 const GOOD_NAME: &str = r#"{"type":"marker","name":"a","blocks":[]}"#;
184 const BAD_NAME: &str = r#"{"type":"marker","name":" a","blocks":[]}"#;
185
186 #[test]
187 fn heading_level_is_checked_by_the_json_decoder() {
188 let block: MarkdownBlock = facet_json::from_str(GOOD_LEVEL).unwrap();
189 assert!(matches!(
190 block,
191 MarkdownBlock::Heading {
192 level: HeadingLevel::H2,
193 content: _,
194 }
195 ));
196 assert_eq!(facet_json::to_string(&block).unwrap(), GOOD_LEVEL);
197 assert!(facet_json::from_str::<MarkdownBlock>(BAD_LEVEL).is_err());
198 }
199
200 #[test]
201 fn heading_level_is_checked_by_the_value_decoder() {
202 let good: Value = facet_json::from_str(GOOD_LEVEL).unwrap();
203 let block: MarkdownBlock = facet_value::from_value(good).unwrap();
204 assert!(matches!(
205 block,
206 MarkdownBlock::Heading {
207 level: HeadingLevel::H2,
208 content: _,
209 }
210 ));
211 let bad: Value = facet_json::from_str(BAD_LEVEL).unwrap();
212 assert!(facet_value::from_value::<MarkdownBlock>(bad).is_err());
213 }
214
215 #[test]
216 fn marker_name_is_checked_by_both_decoders() {
217 assert!(facet_json::from_str::<MarkdownSelector>(GOOD_NAME).is_ok());
218 assert!(facet_json::from_str::<MarkdownSelector>(BAD_NAME).is_err());
219 let good: Value = facet_json::from_str(GOOD_NAME).unwrap();
220 assert!(facet_value::from_value::<MarkdownSelector>(good).is_ok());
221 let bad: Value = facet_json::from_str(BAD_NAME).unwrap();
222 assert!(facet_value::from_value::<MarkdownSelector>(bad).is_err());
223 }
224}