diff --git a/src/fmt_md.rs b/src/fmt_md.rs index 52d9d1c..ff3ffcf 100644 --- a/src/fmt_md.rs +++ b/src/fmt_md.rs @@ -583,15 +583,7 @@ impl<'a> MdWriterState<'a> { { let Some(title) = title else { return }; out.write_char(' '); - // TODO pick which quoting char to use (single, double, or paren) depending on title - out.write_char('"'); - for ch in title.chars() { - if ch == '"' { - out.write_char('\\'); - } - out.write_char(ch); - } - out.write_char('"'); + TitleQuote::find_best_strategy(title).escape_to(title, out); } fn line_to_string(&mut self, line: &'a [E]) -> String @@ -604,6 +596,53 @@ impl<'a> MdWriterState<'a> { } } +enum TitleQuote { + Double, + Single, + Paren, +} + +impl TitleQuote { + pub fn find_best_strategy(text: &str) -> Self { + [Self::Double, Self::Single, Self::Paren] + .into_iter() + .find(|strategy| !strategy.has_conflicts(text)) + .unwrap_or(TitleQuote::Double) + } + + fn get_surround_chars(&self) -> (char, char) { + match self { + TitleQuote::Double => ('"', '"'), + TitleQuote::Single => ('\'', '\''), + TitleQuote::Paren => ('(', ')'), + } + } + + fn get_conflict_char_fn(surrounds: (char, char)) -> impl Fn(char) -> bool { + let (open, close) = surrounds; + move |ch| ch == open || ch == close + } + + fn has_conflicts(&self, text: &str) -> bool { + text.chars().any(Self::get_conflict_char_fn(self.get_surround_chars())) + } + + fn escape_to(&self, text: &str, out: &mut Output) { + let surrounds = self.get_surround_chars(); + let conflict_char_fn = Self::get_conflict_char_fn(surrounds); + let (open, close) = surrounds; + + out.write_char(open); + for ch in text.chars() { + if conflict_char_fn(ch) { + out.write_char('\\'); + } + out.write_char(ch); + } + out.write_char(close); + } +} + enum DefinitionsToWrite { // simple enum-set-like definition Links, @@ -676,6 +715,49 @@ pub mod tests { Table(_), }); + mod title_quoting { + use super::*; + use crate::fmt_md::TitleQuote; + + crate::variants_checker!(TITLE_QUOTING_CHECKER = TitleQuote { Double, Single, Paren }); + + #[test] + fn bareword_uses_double() { + check("foo", "\"foo\""); + } + + #[test] + fn has_double_quotes() { + check("foo\"bar", "'foo\"bar'"); + } + + #[test] + fn has_double_quotes_and_singles() { + check("foo'\"bar", "(foo'\"bar)"); + } + + #[test] + fn has_only_single_quotes() { + check("foo'bar", "\"foo'bar\""); + } + + #[test] + fn has_all_delimiters() { + check("foo('\")bar", "\"foo('\\\")bar\""); + } + + fn check(input: &str, expected: &str) { + let strategy = TitleQuote::find_best_strategy(input); + TITLE_QUOTING_CHECKER.see(&strategy); + + // +1 to give room for some quotes + let mut writer = Output::new(String::with_capacity(input.len() + 4)); + strategy.escape_to(input, &mut writer); + let actual = writer.take_underlying().unwrap(); + assert_eq!(&actual, expected); + } + } + #[test] fn empty() { check_render(vec![], indoc! {r#""#}); diff --git a/src/select/api.rs b/src/select/api.rs index 5dab1da..b1dfa78 100644 --- a/src/select/api.rs +++ b/src/select/api.rs @@ -242,7 +242,3 @@ impl MdqRefSelector { } } } - -/* - -*/