1use anyhow::{anyhow, Result};
2use std::borrow::Cow;
3
4use crate::{
5 book_config::OnFailure,
6 render::Admonition,
7 resolve::AdmonitionMeta,
8 types::{BuiltinDirective, CssId, Overrides},
9};
10
11pub(crate) fn parse_admonition<'a>(
21 info_string: &'a str,
22 overrides: &'a Overrides,
23 content: &'a str,
24 on_failure: OnFailure,
25 indent: usize,
26) -> Option<Result<Admonition<'a>>> {
27 let extracted = extract_admonish_body(content);
29
30 let info = AdmonitionMeta::from_info_string(info_string, overrides)?;
31 let info = match info {
32 Ok(info) => info,
33 Err(message) => {
34 let fence = extracted.fence;
37 let enclosing_fence: String = std::iter::repeat(fence.character)
38 .take(fence.length + 1)
39 .collect();
40 return Some(match on_failure {
41 OnFailure::Continue => {
42 log::warn!(
43 r#"Error processing admonition. To fail the build instead of continuing, set 'on_failure = "bail"'"#
44 );
45 Ok(Admonition {
46 directive: BuiltinDirective::Bug.to_string(),
47 title: "Error rendering admonishment".to_owned(),
48 css_id: CssId::Prefix("admonition-".to_owned()),
49 additional_classnames: Vec::new(),
50 collapsible: false,
51 content: Cow::Owned(format!(
52 r#"Failed with:
53
54```log
55{message}
56```
57
58Original markdown input:
59
60{enclosing_fence}markdown
61{content}
62{enclosing_fence}
63"#
64 )),
65 indent,
66 })
67 }
68 OnFailure::Bail => Err(anyhow!("Error processing admonition, bailing:\n{content}")),
69 });
70 }
71 };
72
73 Some(Ok(Admonition::new(
74 info,
75 extracted.body,
76 indent,
90 )))
91}
92
93fn extract_admonish_body_start_index(content: &str) -> usize {
99 let index = content
100 .find('\n')
101 .map(|index| index + 1);
103
104 match index {
106 None => 0,
108 Some(index) => {
109 if index > (content.len() - 1) {
111 0
112 } else {
113 index
114 }
115 }
116 }
117}
118
119fn extract_admonish_body_end_index(content: &str) -> (usize, Fence) {
120 let fence_character = content.chars().next_back().unwrap_or('`');
121 let number_fence_characters = content
122 .chars()
123 .rev()
124 .position(|c| c != fence_character)
125 .unwrap_or_default();
126 let fence = Fence::new(fence_character, number_fence_characters);
127
128 let index = content.len() - fence.length;
129 (index, fence)
130}
131
132#[derive(Debug, PartialEq)]
133struct Fence {
134 character: char,
135 length: usize,
136}
137
138impl Fence {
139 fn new(character: char, length: usize) -> Self {
140 Self { character, length }
141 }
142}
143
144#[derive(Debug, PartialEq)]
145struct Extracted<'a> {
146 body: &'a str,
147 fence: Fence,
148}
149
150fn extract_admonish_body(content: &str) -> Extracted<'_> {
157 let start_index = extract_admonish_body_start_index(content);
158 let (end_index, fence) = extract_admonish_body_end_index(content);
159
160 let admonish_content = &content[start_index..end_index];
161 let body = admonish_content.trim_end();
164 Extracted { body, fence }
165}
166
167#[cfg(test)]
168mod test {
169 use super::*;
170 use pretty_assertions::assert_eq;
171
172 #[test]
173 fn test_extract_start() {
174 for (text, expected) in [
175 ("```sane example\ncontent```", 16),
176 ("~~~~~\nlonger fence", 6),
177 ("```\n```", 4),
179 ("```\n", 0),
181 ] {
182 let actual = extract_admonish_body_start_index(text);
183 assert_eq!(actual, expected);
184 }
185 }
186
187 #[test]
188 fn test_extract_end() {
189 for (text, expected) in [
190 ("\n```", (1, Fence::new('`', 3))),
191 ("\n``````", (1, Fence::new('`', 6))),
193 ("\n~~~~", (1, Fence::new('~', 4))),
194 ("\n ```", (4, Fence::new('`', 3))),
196 ("content\n```", (8, Fence::new('`', 3))),
197 ] {
198 let actual = extract_admonish_body_end_index(text);
199 assert_eq!(actual, expected);
200 }
201 }
202
203 #[test]
204 fn test_extract() {
205 fn content_fence(body: &'static str, character: char, length: usize) -> Extracted<'static> {
206 Extracted {
207 body,
208 fence: Fence::new(character, length),
209 }
210 }
211 for (text, expected) in [
212 ("```\n```", content_fence("", '`', 3)),
214 (
216 "```admonish\ncontent\n```",
217 content_fence("content", '`', 3),
218 ),
219 (
221 "```admonish \n content \n ```",
222 content_fence(" content", '`', 3),
223 ),
224 (
226 "``````admonish\ncontent\n``````",
227 content_fence("content", '`', 6),
228 ),
229 (
231 "~~~admonish\ncontent\n~~~~~",
232 content_fence("content", '~', 5),
234 ),
235 ] {
236 let actual = extract_admonish_body(text);
237 assert_eq!(actual, expected);
238 }
239 }
240}