diff --git a/Cargo.lock b/Cargo.lock index f62349b..2e18b61 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -160,9 +160,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "markdown" -version = "1.0.0-alpha.17" +version = "1.0.0-alpha.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e27d6220ce21f80ce5c4201f23a37c6f1ad037c72c9d1ff215c2919605a5d6" +checksum = "8bf98a7fcfa423e67aafbaf1bfe7b689ce064434bfed02fa4d9d6db0fc8cc50b" dependencies = [ "unicode-id", ] diff --git a/Cargo.toml b/Cargo.toml index eb5978e..ba938fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ repository = "https://github.com/yshavit/mdq" [dependencies] clap = { version = "4.5.7", features = ["derive"] } -markdown = "1.0.0-alpha.16" +markdown = "1.0.0-alpha.19" paste = "1.0" regex = "1.10.4" serde = { version = "1", features = ["derive"] } diff --git a/src/fmt_md_inlines.rs b/src/fmt_md_inlines.rs index ce6eeca..da61b17 100644 --- a/src/fmt_md_inlines.rs +++ b/src/fmt_md_inlines.rs @@ -6,6 +6,7 @@ use crate::tree::{ }; use serde::Serialize; use std::borrow::Cow; +use std::cmp::max; use std::collections::{HashMap, HashSet}; #[derive(Debug, Copy, Clone)] @@ -128,15 +129,29 @@ impl<'md> MdInlinesWriter<'md> { out.write_str(surround); } Inline::Text(Text { variant, value }) => { - let surround = match variant { - TextVariant::Plain => "", - TextVariant::Code => "`", - TextVariant::Math => "$", - TextVariant::Html => "", + let (surround_ch, surround_space) = match variant { + TextVariant::Plain => (Cow::Borrowed(""), false), + TextVariant::Math => (Cow::Borrowed("$"), false), + TextVariant::Html => (Cow::Borrowed(""), false), + TextVariant::Code => { + let backticks_info = BackticksInfo::from(value); + let surround_ch = if backticks_info.count == 0 { + Cow::Borrowed("`") + } else { + Cow::Owned("`".repeat(backticks_info.count + 1)) + }; + (surround_ch, backticks_info.at_either_end) + } }; - out.write_str(surround); + out.write_str(&surround_ch); + if surround_space { + out.write_char(' '); + } out.write_str(value); - out.write_str(surround); + if surround_space { + out.write_char(' '); + } + out.write_str(&surround_ch); } Inline::Link(link) => self.write_linklike(out, link), Inline::Image(image) => self.write_linklike(out, image), @@ -225,6 +240,32 @@ impl<'md> MdInlinesWriter<'md> { } } +struct BackticksInfo { + count: usize, + at_either_end: bool, +} + +impl From<&String> for BackticksInfo { + fn from(s: &String) -> Self { + let mut overall_max = 0; + let mut current_stretch = 0; + for c in s.chars() { + match c { + '`' => current_stretch += 1, + _ => { + if current_stretch > 0 { + overall_max = max(current_stretch, overall_max); + current_stretch = 0; + } + } + } + } + let count = max(current_stretch, overall_max); + let at_either_end = s.starts_with('`') || s.ends_with('`'); + Self { count, at_either_end } + } +} + enum TitleQuote { Double, Single, @@ -276,6 +317,9 @@ impl TitleQuote { mod tests { use super::*; use crate::output::Output; + use crate::tree::ReadOptions; + use crate::unwrap; + use crate::utils_for_test::get_only; mod title_quoting { use super::*; @@ -318,4 +362,82 @@ mod tests { assert_eq!(&actual, expected); } } + + mod inline_code { + use super::*; + + #[test] + fn round_trip_no_backticks() { + round_trip_inline("hello world"); + } + + #[test] + fn round_trip_one_backtick() { + round_trip_inline("hello ` world"); + } + + #[test] + #[ignore] // #171 + fn round_trip_one_backtick_at_start() { + round_trip_inline("`hello"); + } + + #[test] + #[ignore] // #171 + fn round_trip_one_backtick_at_end() { + round_trip_inline("hello `"); + } + + #[test] + fn round_trip_three_backticks() { + round_trip_inline("hello ``` world"); + } + + #[test] + #[ignore] // #171 + fn round_trip_three_backticks_at_end() { + round_trip_inline("hello `"); + } + + #[test] + #[ignore] // #171 + fn round_trip_three_backticks_at_start() { + round_trip_inline("`hello"); + } + + #[test] + fn round_trip_surrounding_whitespace() { + round_trip_inline(" hello "); + } + + #[test] + fn round_trip_backtick_and_surrounding_whitespace() { + round_trip_inline(" hello`world "); + } + + fn round_trip_inline(inline_str: &str) { + round_trip(&Inline::Text(Text { + variant: TextVariant::Code, + value: inline_str.to_string(), + })); + } + } + + /// Not a pure unit test; semi-integ. Checks that writing an inline to markdown and then parsing + /// that markdown results in the original inline. + fn round_trip(orig: &Inline) { + let mut output = Output::new(String::new()); + let mut writer = MdInlinesWriter::new(MdInlinesWriterOptions { + link_format: LinkTransform::Keep, + }); + writer.write_inline_element(&mut output, &orig); + let md_str = output.take_underlying().unwrap(); + + let ast = markdown::to_mdast(&md_str, &markdown::ParseOptions::gfm()).unwrap(); + let md_tree = MdElem::read(ast, &ReadOptions::default()).unwrap(); + + unwrap!(&md_tree[0], MdElem::Paragraph(p)); + let parsed = get_only(&p.body); + assert_eq!(parsed, orig); + } } diff --git a/src/utils_for_test.rs b/src/utils_for_test.rs index 38b6230..8192555 100644 --- a/src/utils_for_test.rs +++ b/src/utils_for_test.rs @@ -1,7 +1,29 @@ +#[cfg(test)] +pub use test_utils::*; + // We this file's contents from prod by putting them in a submodule guarded by cfg(test), but then "pub use" it to // export its contents. #[cfg(test)] mod test_utils { + use std::fmt::Debug; + + pub fn get_only>(col: C) -> T { + let mut iter = col.into_iter(); + let Some(result) = iter.next() else { + panic!("expected an element, but was empty"); + }; + match iter.next() { + None => result, + Some(extra) => { + let mut all = Vec::new(); + all.push(result); + all.push(extra); + all.extend(iter); + panic!("expected exactly one element, but found {}: {all:?}", all.len()); + } + } + } + /// Turn a pattern match into an `if let ... { else panic! }`. #[macro_export] macro_rules! unwrap {