razeri_plugin_contract/
comment.rs1#[must_use]
35pub fn block_comment(text: &str) -> String {
36 let safe = escaped(text);
37 let lines: Vec<&str> = safe.lines().map(str::trim_end).collect();
38 let first = lines.iter().position(|line| !line.is_empty());
39 let last = lines.iter().rposition(|line| !line.is_empty());
40 let (Some(first), Some(last)) = (first, last) else {
41 return "/* */".to_owned();
42 };
43 let lines = &lines[first..=last];
44 if let [only] = lines {
45 return format!("/* {only} */");
46 }
47 let mut out = String::with_capacity(safe.len() + lines.len() * 4 + 6);
48 for (at, line) in lines.iter().enumerate() {
49 if at == 0 {
50 out.push_str("/* ");
51 } else {
52 out.push('\n');
53 if !line.is_empty() {
54 out.push_str(BLOCK_COMMENT_HANG);
55 }
56 }
57 out.push_str(line);
58 }
59 out.push_str(" */");
60 out
61}
62
63#[must_use]
74pub fn cron_in_comment(expression: &str) -> String {
75 const FIVE: [(u32, u32); 5] = [(0, 59), (0, 23), (1, 31), (1, 12), (0, 6)];
76 const SEVEN: [(u32, u32); 7] =
77 [(0, 59), (0, 59), (0, 23), (1, 31), (1, 12), (1, 7), (1970, 2099)];
78 let fields: Vec<&str> = expression.split_whitespace().collect();
79 let ranges: &[(u32, u32)] = match fields.len() {
80 5 => &FIVE,
81 6 | 7 => &SEVEN[..fields.len()],
82 _ => return expression.to_owned(),
83 };
84 fields
85 .iter()
86 .zip(ranges)
87 .map(|(field, (low, high))| {
88 field
89 .split(',')
90 .map(|part| match part.strip_prefix("*/") {
91 Some(step) => format!("{low}-{high}/{step}"),
92 None => part.to_owned(),
93 })
94 .collect::<Vec<_>>()
95 .join(",")
96 })
97 .collect::<Vec<_>>()
98 .join(" ")
99}
100
101pub const BLOCK_COMMENT_HANG: &str = " ";
104
105fn escaped(text: &str) -> String {
107 let mut out = String::with_capacity(text.len());
108 let mut chars = text.chars().peekable();
109 while let Some(c) = chars.next() {
110 out.push(c);
111 let next = chars.peek().copied();
112 if (c == '*' && next == Some('/')) || (c == '/' && next == Some('*')) {
113 out.push(' ');
114 }
115 }
116 out
117}
118
119#[must_use]
126pub fn comment_lines_apart(statement: &str) -> (Vec<&str>, Vec<&str>) {
127 let mut notes = Vec::new();
128 let mut body = Vec::new();
129 for (line, is_note) in statement.lines().zip(comment_line_flags(statement)) {
130 if is_note {
131 notes.push(line);
132 } else {
133 body.push(line);
134 }
135 }
136 (notes, body)
137}
138
139#[must_use]
142pub fn comment_line_flags(text: &str) -> Vec<bool> {
143 let mut flags = Vec::new();
144 let mut open = false;
145 for line in text.lines() {
146 let trimmed = line.trim_start();
147 let line_comment = !open && trimmed.starts_with("--");
148 let is_note = open || line_comment || trimmed.starts_with("/*");
149 if is_note && !line_comment {
150 let mut depth = usize::from(open);
152 let mut chars = trimmed.chars().peekable();
153 while let Some(c) = chars.next() {
154 if c == '/' && chars.peek() == Some(&'*') {
155 chars.next();
156 depth += 1;
157 } else if c == '*' && chars.peek() == Some(&'/') {
158 chars.next();
159 depth = depth.saturating_sub(1);
160 }
161 }
162 open = depth > 0;
163 }
164 flags.push(is_note);
165 }
166 flags
167}
168
169#[cfg(test)]
170mod tests {
171
172 #[test]
173 fn a_cron_step_over_a_whole_field_keeps_its_schedule_and_closes_nothing() {
174 assert_eq!(cron_in_comment("0 */6 * * *"), "0 0-23/6 * * *");
175 assert_eq!(cron_in_comment("*/15 * * * 1-5"), "0-59/15 * * * 1-5");
176 assert_eq!(cron_in_comment("0 0 */2 * * ?"), "0 0 0-23/2 * * ?");
177 assert_eq!(cron_in_comment("0 3 * * *"), "0 3 * * *");
178 assert_eq!(cron_in_comment("@daily"), "@daily");
179 let said = block_comment(&format!("Schedule: cron {}", cron_in_comment("0 */6 * * *")));
180 assert_eq!(said, "/* Schedule: cron 0 0-23/6 * * * */");
181 }
182 use super::*;
183
184 #[test]
185 fn one_line_is_one_block() {
186 assert_eq!(
187 block_comment("Merge in new and updated rows"),
188 "/* Merge in new and updated rows */"
189 );
190 }
191
192 #[test]
193 fn several_lines_hang_under_the_first() {
194 assert_eq!(
195 block_comment("Stop if a row has no key.\nNothing is written.\n\nThe end."),
196 "/* Stop if a row has no key.\n Nothing is written.\n\n The end. */"
197 );
198 }
199
200 #[test]
201 fn a_closing_mark_in_the_text_cannot_end_the_comment() {
202 let rendered = block_comment("a pattern such as '*/' or '/*' or */*/");
203 let inner = &rendered[3..rendered.len() - 3];
204 assert!(!inner.contains("*/") && !inner.contains("/*"), "{rendered}");
205 assert_eq!(rendered, "/* a pattern such as '* /' or '/ *' or * / * / */");
206 }
207
208 #[test]
209 fn a_comment_is_never_a_hint_or_a_version_comment() {
210 assert!(block_comment("!50001 not a version comment").starts_with("/* !"));
211 assert!(block_comment("+ INDEX(t) not a hint").starts_with("/* +"));
212 }
213
214 #[test]
215 fn empty_text_is_an_empty_comment() {
216 assert_eq!(block_comment(""), "/* */");
217 assert_eq!(block_comment("\n \n"), "/* */");
218 }
219
220 #[test]
221 fn a_rule_of_dashes_inside_a_block_is_the_blocks_own_line() {
222 let text = "/* ----\n Entity: t\n ---- */\nCREATE TABLE t (a INT);";
223 assert_eq!(comment_line_flags(text), [true, true, true, false]);
224 }
225
226 #[test]
227 fn a_statements_notes_come_apart_from_its_lines() {
228 let statement = "/* one\n two */\nCREATE TABLE t (a INT);\n-- three\nSELECT 1";
229 let (notes, body) = comment_lines_apart(statement);
230 assert_eq!(notes, ["/* one", " two */", "-- three"]);
231 assert_eq!(body, ["CREATE TABLE t (a INT);", "SELECT 1"]);
232 }
233
234 #[test]
235 fn trailing_spaces_and_outer_blank_lines_go() {
236 assert_eq!(block_comment("\nfirst \nsecond\n\n"), "/* first\n second */");
237 }
238}