tytanic_core/test/
annotation.rs1use std::str::FromStr;
23
24use ecow::{EcoString, EcoVec};
25use thiserror::Error;
26
27use crate::config::Direction;
28
29#[derive(Debug, Error)]
31pub enum ParseAnnotationError {
32 #[error("the annotation had only one or no delimiter")]
34 MissingDelimiter,
35
36 #[error("unknown or invalid annotation identifier: {0:?}")]
38 Unknown(EcoString),
39
40 #[error("the annotation expected no argument, but received one")]
42 UnexpectedArg(&'static str),
43
44 #[error("the annotation expected an argument, but received none")]
46 MissingArg(&'static str),
47
48 #[error("an error occured while parsing the annotation")]
50 Other(#[source] Box<dyn std::error::Error + Sync + Send + 'static>),
51}
52
53#[derive(Debug, Clone, PartialEq)]
60pub enum Annotation {
61 Skip,
64
65 Dir(Direction),
67
68 Ppi(f32),
70
71 MaxDelta(u8),
73
74 MaxDeviations(usize),
76}
77
78impl Annotation {
79 pub fn collect(source: &str) -> Result<EcoVec<Self>, ParseAnnotationError> {
81 let lines = source.lines().skip_while(|line| {
83 line.strip_prefix("//")
84 .is_some_and(|rest| !rest.starts_with('/'))
85 || line.trim().is_empty()
86 });
87
88 let lines = lines.map_while(|line| line.strip_prefix("///").map(str::trim));
90
91 let lines = lines.filter(|line| !line.is_empty());
93
94 let lines = lines.take_while(|line| line.starts_with('['));
96
97 lines.map(str::parse).collect()
98 }
99}
100
101impl FromStr for Annotation {
102 type Err = ParseAnnotationError;
103
104 fn from_str(s: &str) -> Result<Self, Self::Err> {
105 let Some(rest) = s.strip_prefix('[') else {
106 return Err(ParseAnnotationError::MissingDelimiter);
107 };
108
109 let Some(rest) = rest.strip_suffix(']') else {
110 return Err(ParseAnnotationError::MissingDelimiter);
111 };
112
113 let (id, arg) = match rest.split_once(':') {
114 Some((id, arg)) => (id, Some(arg.trim())),
115 None => (rest, None),
116 };
117
118 match id.trim() {
119 "skip" => {
120 if arg.is_some() {
121 Err(ParseAnnotationError::UnexpectedArg("test"))
122 } else {
123 Ok(Annotation::Skip)
124 }
125 }
126 "dir" => match arg {
127 Some(arg) => match arg.trim() {
128 "ltr" => Ok(Annotation::Dir(Direction::Ltr)),
129 "rtl" => Ok(Annotation::Dir(Direction::Rtl)),
130 _ => Err(ParseAnnotationError::Other(
131 format!("invalid direction {arg:?}, expected one of ltr or rtl").into(),
132 )),
133 },
134 None => Err(ParseAnnotationError::UnexpectedArg("test")),
135 },
136 "ppi" => match arg {
137 Some(arg) => match arg.trim().parse() {
138 Ok(arg) => Ok(Annotation::Ppi(arg)),
139 Err(err) => Err(ParseAnnotationError::Other(err.into())),
140 },
141 None => Err(ParseAnnotationError::UnexpectedArg("test")),
142 },
143 "max-delta" => match arg {
144 Some(arg) => match arg.trim().parse() {
145 Ok(arg) => Ok(Annotation::MaxDelta(arg)),
146 Err(err) => Err(ParseAnnotationError::Other(err.into())),
147 },
148 None => Err(ParseAnnotationError::UnexpectedArg("test")),
149 },
150 "max-deviations" => match arg {
151 Some(arg) => match arg.trim().parse() {
152 Ok(arg) => Ok(Annotation::MaxDeviations(arg)),
153 Err(err) => Err(ParseAnnotationError::Other(err.into())),
154 },
155 None => Err(ParseAnnotationError::UnexpectedArg("test")),
156 },
157 _ => Err(ParseAnnotationError::Unknown(id.into())),
158 }
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn test_annotation_from_str() {
168 assert_eq!(Annotation::from_str("[skip]").unwrap(), Annotation::Skip);
169 assert_eq!(Annotation::from_str("[ skip ]").unwrap(), Annotation::Skip);
170
171 assert!(Annotation::from_str("[ skip ").is_err());
172 assert!(Annotation::from_str("[unknown]").is_err());
173 }
174
175 #[test]
176 fn test_annotation_unexpected_arg() {
177 assert!(Annotation::from_str("[skip:]").is_err());
178 assert!(Annotation::from_str("[skip: 10]").is_err());
179 }
180
181 #[test]
182 fn test_annotation_expected_arg() {
183 assert!(Annotation::from_str("[ppi]").is_err());
184 assert!(Annotation::from_str("[max-delta:]").is_err());
185 }
186
187 #[test]
188 fn test_annotation_arg() {
189 assert_eq!(
190 Annotation::from_str("[max-deviations: 20]").unwrap(),
191 Annotation::MaxDeviations(20)
192 );
193 assert_eq!(
194 Annotation::from_str("[ppi: 42.5]").unwrap(),
195 Annotation::Ppi(42.5)
196 );
197 }
198
199 #[test]
200 fn test_collect_book_example() {
201 let source = "\
202 /// [skip] \n\
203 /// \n\
204 /// Synopsis: \n\
205 /// ... \n\
206 \n\
207 #import \"/src/internal.typ\": foo \n\
208 ...";
209
210 assert_eq!(Annotation::collect(source).unwrap(), [Annotation::Skip]);
211 }
212
213 #[test]
214 fn test_collect_issue_109() {
215 assert_eq!(
216 Annotation::collect("///[skip]").unwrap(),
217 [Annotation::Skip]
218 );
219 assert_eq!(Annotation::collect("///").unwrap(), []);
220 assert_eq!(
221 Annotation::collect("/// [skip]").unwrap(),
222 [Annotation::Skip]
223 );
224 assert_eq!(
225 Annotation::collect("///[skip]\n///").unwrap(),
226 [Annotation::Skip]
227 );
228 }
229}