#[must_use]
pub fn block_comment(text: &str) -> String {
let safe = escaped(text);
let lines: Vec<&str> = safe.lines().map(str::trim_end).collect();
let first = lines.iter().position(|line| !line.is_empty());
let last = lines.iter().rposition(|line| !line.is_empty());
let (Some(first), Some(last)) = (first, last) else {
return "/* */".to_owned();
};
let lines = &lines[first..=last];
if let [only] = lines {
return format!("/* {only} */");
}
let mut out = String::with_capacity(safe.len() + lines.len() * 4 + 6);
for (at, line) in lines.iter().enumerate() {
if at == 0 {
out.push_str("/* ");
} else {
out.push('\n');
if !line.is_empty() {
out.push_str(BLOCK_COMMENT_HANG);
}
}
out.push_str(line);
}
out.push_str(" */");
out
}
#[must_use]
pub fn cron_in_comment(expression: &str) -> String {
const FIVE: [(u32, u32); 5] = [(0, 59), (0, 23), (1, 31), (1, 12), (0, 6)];
const SEVEN: [(u32, u32); 7] =
[(0, 59), (0, 59), (0, 23), (1, 31), (1, 12), (1, 7), (1970, 2099)];
let fields: Vec<&str> = expression.split_whitespace().collect();
let ranges: &[(u32, u32)] = match fields.len() {
5 => &FIVE,
6 | 7 => &SEVEN[..fields.len()],
_ => return expression.to_owned(),
};
fields
.iter()
.zip(ranges)
.map(|(field, (low, high))| {
field
.split(',')
.map(|part| match part.strip_prefix("*/") {
Some(step) => format!("{low}-{high}/{step}"),
None => part.to_owned(),
})
.collect::<Vec<_>>()
.join(",")
})
.collect::<Vec<_>>()
.join(" ")
}
pub const BLOCK_COMMENT_HANG: &str = " ";
fn escaped(text: &str) -> String {
let mut out = String::with_capacity(text.len());
let mut chars = text.chars().peekable();
while let Some(c) = chars.next() {
out.push(c);
let next = chars.peek().copied();
if (c == '*' && next == Some('/')) || (c == '/' && next == Some('*')) {
out.push(' ');
}
}
out
}
#[must_use]
pub fn comment_lines_apart(statement: &str) -> (Vec<&str>, Vec<&str>) {
let mut notes = Vec::new();
let mut body = Vec::new();
for (line, is_note) in statement.lines().zip(comment_line_flags(statement)) {
if is_note {
notes.push(line);
} else {
body.push(line);
}
}
(notes, body)
}
#[must_use]
pub fn comment_line_flags(text: &str) -> Vec<bool> {
let mut flags = Vec::new();
let mut open = false;
for line in text.lines() {
let trimmed = line.trim_start();
let line_comment = !open && trimmed.starts_with("--");
let is_note = open || line_comment || trimmed.starts_with("/*");
if is_note && !line_comment {
let mut depth = usize::from(open);
let mut chars = trimmed.chars().peekable();
while let Some(c) = chars.next() {
if c == '/' && chars.peek() == Some(&'*') {
chars.next();
depth += 1;
} else if c == '*' && chars.peek() == Some(&'/') {
chars.next();
depth = depth.saturating_sub(1);
}
}
open = depth > 0;
}
flags.push(is_note);
}
flags
}
#[cfg(test)]
mod tests {
#[test]
fn a_cron_step_over_a_whole_field_keeps_its_schedule_and_closes_nothing() {
assert_eq!(cron_in_comment("0 */6 * * *"), "0 0-23/6 * * *");
assert_eq!(cron_in_comment("*/15 * * * 1-5"), "0-59/15 * * * 1-5");
assert_eq!(cron_in_comment("0 0 */2 * * ?"), "0 0 0-23/2 * * ?");
assert_eq!(cron_in_comment("0 3 * * *"), "0 3 * * *");
assert_eq!(cron_in_comment("@daily"), "@daily");
let said = block_comment(&format!("Schedule: cron {}", cron_in_comment("0 */6 * * *")));
assert_eq!(said, "/* Schedule: cron 0 0-23/6 * * * */");
}
use super::*;
#[test]
fn one_line_is_one_block() {
assert_eq!(
block_comment("Merge in new and updated rows"),
"/* Merge in new and updated rows */"
);
}
#[test]
fn several_lines_hang_under_the_first() {
assert_eq!(
block_comment("Stop if a row has no key.\nNothing is written.\n\nThe end."),
"/* Stop if a row has no key.\n Nothing is written.\n\n The end. */"
);
}
#[test]
fn a_closing_mark_in_the_text_cannot_end_the_comment() {
let rendered = block_comment("a pattern such as '*/' or '/*' or */*/");
let inner = &rendered[3..rendered.len() - 3];
assert!(!inner.contains("*/") && !inner.contains("/*"), "{rendered}");
assert_eq!(rendered, "/* a pattern such as '* /' or '/ *' or * / * / */");
}
#[test]
fn a_comment_is_never_a_hint_or_a_version_comment() {
assert!(block_comment("!50001 not a version comment").starts_with("/* !"));
assert!(block_comment("+ INDEX(t) not a hint").starts_with("/* +"));
}
#[test]
fn empty_text_is_an_empty_comment() {
assert_eq!(block_comment(""), "/* */");
assert_eq!(block_comment("\n \n"), "/* */");
}
#[test]
fn a_rule_of_dashes_inside_a_block_is_the_blocks_own_line() {
let text = "/* ----\n Entity: t\n ---- */\nCREATE TABLE t (a INT);";
assert_eq!(comment_line_flags(text), [true, true, true, false]);
}
#[test]
fn a_statements_notes_come_apart_from_its_lines() {
let statement = "/* one\n two */\nCREATE TABLE t (a INT);\n-- three\nSELECT 1";
let (notes, body) = comment_lines_apart(statement);
assert_eq!(notes, ["/* one", " two */", "-- three"]);
assert_eq!(body, ["CREATE TABLE t (a INT);", "SELECT 1"]);
}
#[test]
fn trailing_spaces_and_outer_blank_lines_go() {
assert_eq!(block_comment("\nfirst \nsecond\n\n"), "/* first\n second */");
}
}