1use std::borrow::Cow;
2
3pub const COMMENT_START: &str = "<yolk> ";
4
5use crate::util::create_regex;
6
7use super::element::{Block, Element};
8
9const PREFIX_COMMENT_SYMBOLS: [&str; 8] = ["//", "#", "--", ";", "%", "\"", "'", "rem"];
10const CIRCUMFIX_COMMENT_SYMBOLS: [(&str, &str); 5] = [
11 ("/*", "*/"),
12 ("<!--", "-->"),
13 ("{-", "-}"),
14 ("--[[", "]]"),
15 ("(", ")"),
16];
17
18#[derive(Debug, Clone, Eq, PartialEq, arbitrary::Arbitrary)]
19pub enum CommentStyle {
20 Prefix(String),
21 Circumfix(String, String),
22}
23
24impl Default for CommentStyle {
25 fn default() -> Self {
26 CommentStyle::Prefix("#".to_string())
27 }
28}
29
30impl CommentStyle {
39 pub fn try_infer(element: &Element<'_>) -> Option<Self> {
41 let line = match &element {
42 Element::Inline { line, .. }
43 | Element::NextLine {
44 tagged_line: line, ..
45 }
46 | Element::MultiLine {
47 block: Block {
48 tagged_line: line, ..
49 },
50 ..
51 } => line,
52 Element::Conditional { blocks, .. } => &blocks.first()?.tagged_line,
53 Element::Plain(_) => return None,
54 };
55 let (left, right) = (line.left, line.right);
56
57 for (prefix, postfix) in &CIRCUMFIX_COMMENT_SYMBOLS {
58 if left.trim_end().ends_with(prefix) && right.trim_start().starts_with(postfix) {
59 return Some(CommentStyle::Circumfix(
60 prefix.to_string(),
61 postfix.to_string(),
62 ));
63 }
64 }
65 for prefix in &PREFIX_COMMENT_SYMBOLS {
66 if left.trim_end().ends_with(prefix) {
67 return Some(CommentStyle::Prefix(prefix.to_string()));
68 }
69 }
70 None
71 }
72
73 pub fn try_infer_from_elements(elements: &[Element<'_>]) -> Option<Self> {
74 for e in elements {
75 if let Some(style) = Self::try_infer(e) {
76 return Some(style);
77 }
78 }
79 None
80 }
81
82 #[allow(unused)]
83 pub fn prefix(left: &str) -> Self {
84 CommentStyle::Prefix(left.to_string())
85 }
86 #[allow(unused)]
87 pub fn circumfix(left: &str, right: &str) -> Self {
88 CommentStyle::Circumfix(left.to_string(), right.to_string())
89 }
90 pub fn left(&self) -> &str {
91 match self {
92 CommentStyle::Prefix(left) => left,
93 CommentStyle::Circumfix(left, _) => left,
94 }
95 }
96
97 pub fn toggle_string(&self, s: &str, enable: bool) -> String {
98 s.split('\n')
100 .map(|x| self.toggle_line(x, enable))
101 .collect::<Vec<_>>()
102 .join("\n")
103 }
104
105 pub fn toggle_line<'a>(&self, line: &'a str, enable: bool) -> Cow<'a, str> {
106 if enable {
107 self.enable_line(line)
108 } else {
109 self.disable_line(line)
110 }
111 }
112
113 pub fn enable_line<'a>(&self, line: &'a str) -> Cow<'a, str> {
114 let left = self.left();
115 let re = create_regex(format!(
116 "{}{}",
117 regex::escape(left),
118 regex::escape(COMMENT_START)
119 ))
120 .unwrap();
121 let left_done = re.replace_all(line, "");
122 if let CommentStyle::Circumfix(_, right) = self {
123 let re_right = create_regex(regex::escape(right)).unwrap();
124 Cow::Owned(re_right.replace_all(&left_done, "").to_string())
125 } else {
126 left_done
127 }
128 }
129
130 pub fn is_disabled(&self, line: &str) -> bool {
131 let re = match self {
132 CommentStyle::Prefix(left) => {
133 format!("^.*{}{}", regex::escape(left), regex::escape(COMMENT_START))
134 }
135 CommentStyle::Circumfix(left, right) => format!(
136 "^.*{}{}.*{}",
137 regex::escape(left),
138 regex::escape(COMMENT_START),
139 regex::escape(right)
140 ),
141 };
142 create_regex(re).unwrap().is_match(line)
143 }
144
145 pub fn disable_line<'a>(&self, line: &'a str) -> Cow<'a, str> {
146 if self.is_disabled(line) || line.trim().is_empty() {
147 return line.into();
148 }
149 let left = self.left();
150 let re = create_regex("^(\\s*)(.*)$").unwrap();
151 let (indent, remaining_line) = re
152 .captures(line)
153 .and_then(|x| x.get(1).zip(x.get(2)))
154 .map(|(a, b)| (a.as_str(), b.as_str()))
155 .unwrap_or_default();
156 let right = match self {
157 CommentStyle::Prefix(_) => "".to_string(),
158 CommentStyle::Circumfix(_, right) => right.to_string(),
159 };
160 format!("{indent}{left}{COMMENT_START}{remaining_line}{right}",).into()
161 }
162}
163
164#[cfg(test)]
165mod test {
166 use crate::util::test_util::TestResult;
167
168 use crate::templating::element::Element;
169
170 use super::CommentStyle;
171
172 use pretty_assertions::assert_eq;
173 use rstest::rstest;
174
175 #[rstest]
176 #[case(" foo", " #<yolk> foo", CommentStyle::prefix("#"))]
177 #[case(" foo", " /*<yolk> foo*/", CommentStyle::circumfix("/*", "*/"))]
178 #[case("foo", "#<yolk> foo", CommentStyle::prefix("#"))]
179 #[case("foo", "/*<yolk> foo*/", CommentStyle::circumfix("/*", "*/"))]
180 fn test_disable_enable_roundtrip(
181 #[case] start: &str,
182 #[case] expected_disabled: &str,
183 #[case] comment_style: CommentStyle,
184 ) {
185 let disabled = comment_style.disable_line(start);
186 let enabled = comment_style.enable_line(disabled.as_ref());
187 assert_eq!(expected_disabled, disabled);
188 assert_eq!(start, enabled);
189 }
190
191 #[rstest]
192 #[case(CommentStyle::prefix("#"), "\tfoo")]
193 #[case(CommentStyle::prefix("#"), "foo ")]
194 #[case(CommentStyle::circumfix("/*", "*/"), " foo ")]
195 fn test_enable_idempotent(#[case] comment_style: CommentStyle, #[case] line: &str) {
196 let enabled = comment_style.enable_line(line);
197 let enabled_again = comment_style.enable_line(enabled.as_ref());
198 assert_eq!(enabled, enabled_again);
199 }
200
201 #[rstest]
202 #[case(CommentStyle::prefix("#"), "\tfoo")]
203 #[case(CommentStyle::prefix("#"), "foo ")]
204 #[case(CommentStyle::circumfix("/*", "*/"), " foo ")]
205 fn test_disable_idempotent(#[case] comment_style: CommentStyle, #[case] line: &str) {
206 let disabled = comment_style.disable_line(line);
207 let disabled_again = comment_style.disable_line(disabled.as_ref());
208 assert_eq!(disabled, disabled_again);
209 }
210
211 #[rstest]
212 #[case("# {< foo >}", Some(CommentStyle::prefix("#")))]
213 #[case("/* {< foo >} */", Some(CommentStyle::circumfix("/*", "*/")))]
214 fn test_infer_comment_syntax(
215 #[case] input: &str,
216 #[case] expected: Option<CommentStyle>,
217 ) -> TestResult {
218 assert_eq!(
219 CommentStyle::try_infer(&Element::try_from_str(input)?),
220 expected
221 );
222 Ok(())
223 }
224}