use std::fmt::Write as _;
use rustc_lexer::{FrontmatterAllowed, TokenKind, tokenize};
pub(super) struct Insertion {
pub(super) start: usize,
pub(super) end: usize,
pub(super) replacement: String,
}
pub(super) fn build_reason_insertion(snippet: &str, escaped: &str) -> Option<Insertion> {
let (open_paren_offset, close_paren_offset) = locate_outermost_parens(snippet)?;
let head = &snippet[..close_paren_offset];
let close_line_start = head.rfind('\n').map_or(0, |index| index + 1);
let close_on_own_line = head[close_line_start..]
.trim_matches([' ', '\t', '\r'])
.is_empty();
if !close_on_own_line {
let trimmed = head.trim_end_matches([' ', '\t', '\r']);
let after_open = trimmed[open_paren_offset + 1..].trim_start_matches([' ', '\t', '\r']);
let replacement = if after_open.is_empty() {
format!(r#"reason = "{escaped}""#)
} else if trimmed.ends_with(',') {
format!(r#" reason = "{escaped}","#)
} else {
format!(r#", reason = "{escaped}""#)
};
return Some(Insertion {
start: close_paren_offset,
end: close_paren_offset,
replacement,
});
}
let newline_before_close = head.rfind('\n')?;
let last_content_line_start = head[..newline_before_close]
.rfind('\n')
.map_or(open_paren_offset + 1, |index| index + 1);
let last_content_line = &head[last_content_line_start..newline_before_close];
let indent: String = last_content_line
.chars()
.take_while(|character| matches!(character, ' ' | '\t'))
.collect();
let last_content_trimmed = last_content_line.trim_end_matches([' ', '\t', '\r']);
if last_content_trimmed.ends_with(',') || last_content_trimmed.is_empty() {
let insertion = format!("{indent}reason = \"{escaped}\",\n");
Some(Insertion {
start: newline_before_close + 1,
end: newline_before_close + 1,
replacement: insertion,
})
} else {
let trimmed_end = last_content_line_start + last_content_trimmed.len();
let replacement = format!(",\n{indent}reason = \"{escaped}\",");
Some(Insertion {
start: trimmed_end,
end: newline_before_close,
replacement,
})
}
}
fn locate_outermost_parens(snippet: &str) -> Option<(usize, usize)> {
let mut open: Option<usize> = None;
let mut depth: usize = 0;
let mut offset: usize = 0;
for token in tokenize(snippet, FrontmatterAllowed::No) {
let len = token.len as usize;
match token.kind {
TokenKind::OpenParen => {
if open.is_none() {
open = Some(offset);
}
depth += 1;
}
TokenKind::CloseParen => {
if depth == 0 {
return None;
}
depth -= 1;
if depth == 0 {
return open.map(|open_offset| (open_offset, offset));
}
}
_ => {}
}
offset += len;
}
None
}
pub(super) fn escape_for_rust_string(input: &str) -> String {
let mut out = String::with_capacity(input.len());
for character in input.chars() {
match character {
'\\' => out.push_str(r"\\"),
'"' => out.push_str(r#"\""#),
'\n' => out.push_str(r"\n"),
'\r' => out.push_str(r"\r"),
'\t' => out.push_str(r"\t"),
'\0' => out.push_str(r"\0"),
character if (character as u32) < 0x20 || (character as u32) == 0x7F => {
let _ = write!(out, r"\u{{{:x}}}", character as u32);
}
character => out.push(character),
}
}
out
}
#[cfg(test)]
mod tests;