panache_parser/parser/blocks/
fenced_divs.rs1use crate::parser::utils::helpers::strip_leading_spaces;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub(crate) struct DivFenceInfo {
8 pub attributes: String,
9 pub fence_count: usize,
10}
11
12pub(crate) fn try_parse_div_fence_open(content: &str) -> Option<DivFenceInfo> {
18 let trimmed = strip_leading_spaces(content);
19
20 if !trimmed.starts_with(':') {
22 return None;
23 }
24
25 let colon_count = trimmed.chars().take_while(|&c| c == ':').count();
26
27 if colon_count < 3 {
28 return None;
29 }
30
31 let after_colons = trimmed[colon_count..].trim_start();
33
34 let attributes = if after_colons.starts_with('{') {
41 if let Some(close_idx) = after_colons.find('}') {
43 after_colons[..=close_idx].to_string()
44 } else {
45 return None;
47 }
48 } else if after_colons.is_empty() {
49 return None;
51 } else {
52 let word_end = after_colons
54 .find(|c: char| c.is_whitespace() || c == ':')
55 .unwrap_or(after_colons.len());
56 let (first, rest) = after_colons.split_at(word_end);
57 if first.is_empty() {
58 return None;
59 }
60
61 let trailing = rest.trim();
62 if !trailing.is_empty() {
63 if trailing.chars().any(|c| c != ':') {
64 return None;
65 }
66 if trailing.len() < 3 {
68 return None;
69 }
70 } else {
71 let trailing_colons = after_colons[first.len()..].trim();
73 if !trailing_colons.is_empty() {
74 if trailing_colons.chars().any(|c| c != ':') {
75 return None;
76 }
77 if trailing_colons.len() < 3 {
78 return None;
79 }
80 }
81 }
82
83 first.to_string()
84 };
85
86 Some(DivFenceInfo {
87 attributes,
88 fence_count: colon_count,
89 })
90}
91
92pub(crate) fn is_div_closing_fence(content: &str) -> bool {
95 let trimmed = strip_leading_spaces(content);
96
97 if !trimmed.starts_with(':') {
98 return false;
99 }
100
101 let colon_count = trimmed.chars().take_while(|&c| c == ':').count();
102
103 if colon_count < 3 {
104 return false;
105 }
106
107 trimmed[colon_count..].trim().is_empty()
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn test_parse_div_fence_open_with_curly_braces() {
117 let line = "::: {.callout-note}";
118 let fence = try_parse_div_fence_open(line).unwrap();
119 assert_eq!(fence.attributes, "{.callout-note}");
120 }
121
122 #[test]
123 fn test_parse_div_fence_open_with_class_name() {
124 let line = "::: Warning";
125 let fence = try_parse_div_fence_open(line).unwrap();
126 assert_eq!(fence.attributes, "Warning");
127 }
128
129 #[test]
130 fn test_parse_div_fence_open_with_trailing_colons() {
131 let line = "::::: {#special .sidebar} :::::";
132 let fence = try_parse_div_fence_open(line).unwrap();
133 assert_eq!(fence.attributes, "{#special .sidebar}");
134 }
135
136 #[test]
137 fn test_parse_div_fence_open_with_class_name_and_trailing_colons() {
138 let line = "::: Warning :::";
139 let fence = try_parse_div_fence_open(line).unwrap();
140 assert_eq!(fence.attributes, "Warning");
141 }
142
143 #[test]
144 fn test_opening_fence_empty_attributes() {
145 let line = ":::";
146 assert!(try_parse_div_fence_open(line).is_none());
147 assert!(is_div_closing_fence(line));
148 }
149
150 #[test]
151 fn test_opening_fence_many_colons_empty_attributes() {
152 let line = "::::::::::::::";
153 assert!(try_parse_div_fence_open(line).is_none());
154 assert!(is_div_closing_fence(line));
155 }
156
157 #[test]
158 fn test_not_a_fence_too_few_colons() {
159 let line = ":: something";
160 assert!(try_parse_div_fence_open(line).is_none());
161 assert!(!is_div_closing_fence(line));
162 }
163}