use std::borrow::Cow;
pub const COMMENT_START: &str = "<yolk> ";
use crate::util::create_regex;
use super::element::{Block, Element};
const PREFIX_COMMENT_SYMBOLS: [&str; 8] = ["//", "#", "--", ";", "%", "\"", "'", "rem"];
const CIRCUMFIX_COMMENT_SYMBOLS: [(&str, &str); 5] = [
("/*", "*/"),
("<!--", "-->"),
("{-", "-}"),
("--[[", "]]"),
("(", ")"),
];
#[derive(Debug, Clone, Eq, PartialEq, arbitrary::Arbitrary)]
pub enum CommentStyle {
Prefix(String),
Circumfix(String, String),
}
impl Default for CommentStyle {
fn default() -> Self {
CommentStyle::Prefix("#".to_string())
}
}
impl CommentStyle {
pub fn try_infer(element: &Element<'_>) -> Option<Self> {
let line = match &element {
Element::Inline { line, .. }
| Element::NextLine {
tagged_line: line, ..
}
| Element::MultiLine {
block: Block {
tagged_line: line, ..
},
..
} => line,
Element::Conditional { blocks, .. } => &blocks.first()?.tagged_line,
Element::Plain(_) => return None,
};
let (left, right) = (line.left, line.right);
for (prefix, postfix) in &CIRCUMFIX_COMMENT_SYMBOLS {
if left.trim_end().ends_with(prefix) && right.trim_start().starts_with(postfix) {
return Some(CommentStyle::Circumfix(
prefix.to_string(),
postfix.to_string(),
));
}
}
for prefix in &PREFIX_COMMENT_SYMBOLS {
if left.trim_end().ends_with(prefix) {
return Some(CommentStyle::Prefix(prefix.to_string()));
}
}
None
}
pub fn try_infer_from_elements(elements: &[Element<'_>]) -> Option<Self> {
for e in elements {
if let Some(style) = Self::try_infer(e) {
return Some(style);
}
}
None
}
#[allow(unused)]
pub fn prefix(left: &str) -> Self {
CommentStyle::Prefix(left.to_string())
}
#[allow(unused)]
pub fn circumfix(left: &str, right: &str) -> Self {
CommentStyle::Circumfix(left.to_string(), right.to_string())
}
pub fn left(&self) -> &str {
match self {
CommentStyle::Prefix(left) => left,
CommentStyle::Circumfix(left, _) => left,
}
}
pub fn toggle_string(&self, s: &str, enable: bool) -> String {
s.split('\n')
.map(|x| self.toggle_line(x, enable))
.collect::<Vec<_>>()
.join("\n")
}
pub fn toggle_line<'a>(&self, line: &'a str, enable: bool) -> Cow<'a, str> {
if enable {
self.enable_line(line)
} else {
self.disable_line(line)
}
}
pub fn enable_line<'a>(&self, line: &'a str) -> Cow<'a, str> {
let left = self.left();
let re = create_regex(format!(
"{}{}",
regex::escape(left),
regex::escape(COMMENT_START)
))
.unwrap();
let left_done = re.replace_all(line, "");
if let CommentStyle::Circumfix(_, right) = self {
let re_right = create_regex(regex::escape(right)).unwrap();
Cow::Owned(re_right.replace_all(&left_done, "").to_string())
} else {
left_done
}
}
pub fn is_disabled(&self, line: &str) -> bool {
let re = match self {
CommentStyle::Prefix(left) => {
format!("^.*{}{}", regex::escape(left), regex::escape(COMMENT_START))
}
CommentStyle::Circumfix(left, right) => format!(
"^.*{}{}.*{}",
regex::escape(left),
regex::escape(COMMENT_START),
regex::escape(right)
),
};
create_regex(re).unwrap().is_match(line)
}
pub fn disable_line<'a>(&self, line: &'a str) -> Cow<'a, str> {
if self.is_disabled(line) || line.trim().is_empty() {
return line.into();
}
let left = self.left();
let re = create_regex("^(\\s*)(.*)$").unwrap();
let (indent, remaining_line) = re
.captures(line)
.and_then(|x| (x.get(1).zip(x.get(2))))
.map(|(a, b)| (a.as_str(), b.as_str()))
.unwrap_or_default();
let right = match self {
CommentStyle::Prefix(_) => "".to_string(),
CommentStyle::Circumfix(_, right) => right.to_string(),
};
format!("{indent}{left}{COMMENT_START}{remaining_line}{right}",).into()
}
}
#[cfg(test)]
mod test {
use crate::util::test_util::TestResult;
use crate::templating::element::Element;
use super::CommentStyle;
use pretty_assertions::assert_eq;
use rstest::rstest;
#[rstest]
#[case(" foo", " #<yolk> foo", CommentStyle::prefix("#"))]
#[case(" foo", " /*<yolk> foo*/", CommentStyle::circumfix("/*", "*/"))]
#[case("foo", "#<yolk> foo", CommentStyle::prefix("#"))]
#[case("foo", "/*<yolk> foo*/", CommentStyle::circumfix("/*", "*/"))]
fn test_disable_enable_roundtrip(
#[case] start: &str,
#[case] expected_disabled: &str,
#[case] comment_style: CommentStyle,
) {
let disabled = comment_style.disable_line(start);
let enabled = comment_style.enable_line(disabled.as_ref());
assert_eq!(expected_disabled, disabled);
assert_eq!(start, enabled);
}
#[rstest]
#[case(CommentStyle::prefix("#"), "\tfoo")]
#[case(CommentStyle::prefix("#"), "foo ")]
#[case(CommentStyle::circumfix("/*", "*/"), " foo ")]
fn test_enable_idempotent(#[case] comment_style: CommentStyle, #[case] line: &str) {
let enabled = comment_style.enable_line(line);
let enabled_again = comment_style.enable_line(enabled.as_ref());
assert_eq!(enabled, enabled_again);
}
#[rstest]
#[case(CommentStyle::prefix("#"), "\tfoo")]
#[case(CommentStyle::prefix("#"), "foo ")]
#[case(CommentStyle::circumfix("/*", "*/"), " foo ")]
fn test_disable_idempotent(#[case] comment_style: CommentStyle, #[case] line: &str) {
let disabled = comment_style.disable_line(line);
let disabled_again = comment_style.disable_line(disabled.as_ref());
assert_eq!(disabled, disabled_again);
}
#[rstest]
#[case("# {< foo >}", Some(CommentStyle::prefix("#")))]
#[case("/* {< foo >} */", Some(CommentStyle::circumfix("/*", "*/")))]
fn test_infer_comment_syntax(
#[case] input: &str,
#[case] expected: Option<CommentStyle>,
) -> TestResult {
assert_eq!(
CommentStyle::try_infer(&Element::try_from_str(input)?),
expected
);
Ok(())
}
}