From d8957ff74ad78a69d27412ead5251949baa51b44 Mon Sep 17 00:00:00 2001 From: Yuval Shavit Date: Sun, 30 Jun 2024 00:20:44 -0400 Subject: [PATCH] CLI switch to canonicalize links Add a CLI switch and `fmt_md` logic to canonicalize links. - `keep` -> links stay as they are - `inline` -> all links become `[foo](https://example.com)` - `reference` -> all links become `[foo][1]` This resolves #76 --- src/fmt_md.rs | 154 ++++++++++----- src/link_transform.rs | 429 ++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 14 +- src/utils_for_test.rs | 26 ++- 4 files changed, 568 insertions(+), 55 deletions(-) create mode 100644 src/link_transform.rs diff --git a/src/fmt_md.rs b/src/fmt_md.rs index 9c1f420..4109f87 100644 --- a/src/fmt_md.rs +++ b/src/fmt_md.rs @@ -1,19 +1,23 @@ use clap::ValueEnum; -use std::borrow::Borrow; +use std::borrow::{Borrow, Cow}; use std::cmp::max; use std::collections::{HashMap, HashSet}; -use std::fmt::{Alignment, Display}; +use std::fmt::Alignment; +use std::ops::Deref; -use crate::fmt_str::inlines_to_plain_string; +use crate::link_transform::{LinkLabel, LinkTransform, LinkTransformer}; use crate::output::{Block, Output, SimpleWrite}; use crate::str_utils::{pad_to, standard_align, CountingWriter}; use crate::tree::*; use crate::tree_ref::{ListItemRef, MdElemRef}; -#[derive(Default)] pub struct MdOptions { pub link_reference_placement: ReferencePlacement, pub footnote_reference_placement: ReferencePlacement, + pub link_canonicalization: LinkTransform, + /// note: this is not exposed through the CLI, but is used in [crate::link_transform::inlines_to_string] to + /// suppress the thematic breaks between inlines that are otherwise usually present. + pub add_thematic_breaks: bool, } #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] @@ -79,8 +83,9 @@ where links: HashMap::with_capacity(pending_refs_capacity), footnotes: HashMap::with_capacity(pending_refs_capacity), }, + link_transformer: LinkTransformer::from(options.link_canonicalization), }; - writer_state.write_md(out, nodes, true); + writer_state.write_md(out, nodes, options.add_thematic_breaks); // Always write the pending definitions at the end of the doc. If there were no sections, then BottomOfSection // won't have been triggered, but we still want to write them @@ -93,6 +98,7 @@ struct MdWriterState<'a> { seen_links: HashSet>, seen_footnotes: HashSet<&'a String>, pending_references: PendingReferences<'a>, + link_transformer: LinkTransformer, } struct PendingReferences<'a> { @@ -106,33 +112,6 @@ struct UrlAndTitle<'a> { title: &'a Option, } -#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)] -enum LinkLabel<'a> { - Identifier(&'a String), - Inline(&'a Vec), -} - -impl<'a> LinkLabel<'a> { - fn write_to<'b, W: SimpleWrite>(&self, writer: &mut MdWriterState<'b>, out: &mut Output) - where - 'a: 'b, - { - match self { - LinkLabel::Identifier(text) => out.write_str(text), - LinkLabel::Inline(text) => writer.write_line(out, text), - } - } -} - -impl<'a> Display for LinkLabel<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - LinkLabel::Identifier(s) => f.write_str(*s), - LinkLabel::Inline(inlines) => f.write_str(&inlines_to_plain_string(*inlines)), - } - } -} - impl<'a> MdWriterState<'a> { fn write_md(&mut self, out: &mut Output, nodes: I, add_break: bool) where @@ -199,11 +178,12 @@ impl<'a> MdWriterState<'a> { self.write_inline_element(out, inline); } MdElemRef::Link(link) => { - self.write_link_inline_portion(out, LinkLabel::Inline(&link.text), &link.link_definition); + self.write_link_inline_portion(out, LinkLabel::Inline(&link.text), &link.link_definition) } MdElemRef::Image(image) => { out.write_char('!'); - self.write_link_inline_portion(out, LinkLabel::Identifier(&image.alt), &image.link); + let alt_text = &image.alt; + self.write_link_inline_portion(out, LinkLabel::Text(Cow::from(alt_text)), &image.link); } } } @@ -472,9 +452,16 @@ impl<'a> MdWriterState<'a> { W: SimpleWrite, { out.write_char('['); - label.write_to(self, out); + match &label { + LinkLabel::Text(text) => out.write_str(text), + LinkLabel::Inline(text) => self.write_line(out, text), + } out.write_char(']'); - let reference_to_add = match &link.reference { + + let transformed = self.link_transformer.transform(&link.reference, &label); + let link_ref_owned = transformed.into_owned(); + + let reference_to_add = match link_ref_owned { LinkReference::Inline => { out.write_char('('); out.write_str(&link.url); @@ -484,18 +471,15 @@ impl<'a> MdWriterState<'a> { } LinkReference::Full(identifier) => { out.write_char('['); - out.write_str(identifier); + out.write_str(&identifier); out.write_char(']'); - Some(LinkLabel::Identifier(identifier)) + Some(LinkLabel::Text(Cow::from(identifier))) } LinkReference::Collapsed => { out.write_str("[]"); Some(label) } - LinkReference::Shortcut => { - // no write - Some(label) - } + LinkReference::Shortcut => Some(label), }; if let Some(reference_label) = reference_to_add { @@ -552,7 +536,7 @@ impl<'a> MdWriterState<'a> { for (link_ref, link_def) in defs_to_write { out.write_char('['); match link_ref { - LinkLabel::Identifier(identifier) => out.write_str(identifier), + LinkLabel::Text(identifier) => out.write_str(identifier.deref()), LinkLabel::Inline(text) => self.write_line(out, text), } out.write_str("]: "); @@ -660,6 +644,7 @@ pub mod tests { use indoc::indoc; use crate::fmt_md::MdOptions; + use crate::link_transform::LinkTransform; use crate::m_node; use crate::md_elem; use crate::md_elems; @@ -668,7 +653,7 @@ pub mod tests { use crate::tree::*; use crate::tree_ref::MdElemRef; - use super::write_md; + use super::{write_md, ReferencePlacement}; crate::variants_checker!(VARIANTS_CHECKER = MdElemRef { Doc(..), @@ -1856,6 +1841,33 @@ pub mod tests { [link text two](https://example.com/2)"#}, ); } + + /// see [crate::link_transform::tests] for more extensive tests + #[test] + fn reference_transform_smoke_test() { + check_render_refs_with( + &MdOptions { + link_reference_placement: ReferencePlacement::Section, + footnote_reference_placement: ReferencePlacement::Section, + link_canonicalization: LinkTransform::Reference, + add_thematic_breaks: true, + }, + vec![MdElemRef::Link(&Link { + text: vec![mdq_inline!("link text")], + link_definition: LinkDefinition { + url: "https://example.com".to_string(), + title: None, + reference: LinkReference::Inline, // note! inline, but will be transformed to full + }, + })], + indoc! {r#" + [link text][1] + + ----- + + [1]: https://example.com"#}, + ); + } } mod image { @@ -1915,6 +1927,33 @@ pub mod tests { [2]: https://example.com/2.png"#}, ); } + + /// see [crate::link_transform::tests] for more extensive tests + #[test] + fn reference_transform_smoke_test() { + check_render_refs_with( + &MdOptions { + link_reference_placement: ReferencePlacement::Section, + footnote_reference_placement: ReferencePlacement::Section, + link_canonicalization: LinkTransform::Reference, + add_thematic_breaks: true, + }, + vec![MdElemRef::Image(&Image { + alt: "alt text".to_string(), + link: LinkDefinition { + url: "https://example.com".to_string(), + title: None, + reference: LinkReference::Inline, // note! inline, but will be transformed to full + }, + })], + indoc! {r#" + ![alt text][1] + + ----- + + [1]: https://example.com"#}, + ); + } } mod footnote { @@ -1962,7 +2001,7 @@ pub mod tests { mod annotation_and_footnote_layouts { use super::*; - use crate::fmt_md::ReferencePlacement; + use crate::link_transform::LinkTransform; #[test] fn link_and_footnote() { @@ -2002,6 +2041,8 @@ pub mod tests { &MdOptions { link_reference_placement: ReferencePlacement::Section, footnote_reference_placement: ReferencePlacement::Section, + link_canonicalization: LinkTransform::Keep, + add_thematic_breaks: true, }, link_and_footnote_markdown(), indoc! {r#" @@ -2026,6 +2067,8 @@ pub mod tests { &MdOptions { link_reference_placement: ReferencePlacement::Section, footnote_reference_placement: ReferencePlacement::Doc, + link_canonicalization: LinkTransform::Keep, + add_thematic_breaks: true, }, link_and_footnote_markdown(), indoc! {r#" @@ -2053,6 +2096,8 @@ pub mod tests { &MdOptions { link_reference_placement: ReferencePlacement::Section, footnote_reference_placement: ReferencePlacement::Section, + link_canonicalization: LinkTransform::Keep, + add_thematic_breaks: true, }, md_elems![Block::LeafBlock::Paragraph { body: vec![m_node!(Inline::Link { @@ -2079,6 +2124,8 @@ pub mod tests { &MdOptions { link_reference_placement: ReferencePlacement::Doc, footnote_reference_placement: ReferencePlacement::Section, + link_canonicalization: LinkTransform::Keep, + add_thematic_breaks: true, }, link_and_footnote_markdown(), indoc! {r#" @@ -2106,6 +2153,8 @@ pub mod tests { &MdOptions { link_reference_placement: ReferencePlacement::Doc, footnote_reference_placement: ReferencePlacement::Doc, + link_canonicalization: LinkTransform::Keep, + add_thematic_breaks: true, }, link_and_footnote_markdown(), indoc! {r#" @@ -2132,6 +2181,8 @@ pub mod tests { &MdOptions { link_reference_placement: ReferencePlacement::Doc, footnote_reference_placement: ReferencePlacement::Doc, + link_canonicalization: LinkTransform::Keep, + add_thematic_breaks: true, }, // Define them in the opposite order that we'd expect them md_elems![Block::LeafBlock::Paragraph { @@ -2208,7 +2259,7 @@ pub mod tests { } fn check_render(nodes: Vec, expect: &str) { - check_render_with(&MdOptions::default(), nodes, expect); + check_render_with(&default_opts(), nodes, expect); } fn check_render_with(options: &MdOptions, nodes: Vec, expect: &str) { @@ -2220,7 +2271,7 @@ pub mod tests { } fn check_render_refs(nodes: Vec, expect: &str) { - check_render_refs_with(&MdOptions::default(), nodes, expect) + check_render_refs_with(&default_opts(), nodes, expect) } fn check_render_refs_with<'a>(options: &MdOptions, nodes: Vec>, expect: &str) { @@ -2231,4 +2282,13 @@ pub mod tests { let actual = out.take_underlying().unwrap(); assert_eq!(&actual, expect); } + + fn default_opts() -> MdOptions { + MdOptions { + link_reference_placement: Default::default(), + footnote_reference_placement: Default::default(), + link_canonicalization: LinkTransform::Keep, + add_thematic_breaks: true, + } + } } diff --git a/src/link_transform.rs b/src/link_transform.rs new file mode 100644 index 0000000..abba7b8 --- /dev/null +++ b/src/link_transform.rs @@ -0,0 +1,429 @@ +use crate::fmt_md::{write_md, MdOptions, ReferencePlacement}; +use crate::output::Output; +use crate::tree::{Inline, LinkReference}; +use crate::tree_ref::MdElemRef; +use clap::ValueEnum; +use std::borrow::Cow; +use std::collections::hash_map::Entry; +use std::collections::HashMap; +use std::fmt::Display; +use std::ops::Deref; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] +pub enum LinkTransform { + /// Keep links as they were in the original + Keep, + + /// Turn all links into inlined form: `[link text](https://example.com)` + Inline, + + /// Turn all links into reference form: `[link text][1]` + /// + /// Links that weren't already in reference form will be auto-assigned a reference id. Links that were in reference + /// form will have the link number be reordered. + Reference, +} + +pub struct LinkTransformer { + delegate: LinkTransformState, +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub enum LinkLabel<'a> { + Text(Cow<'a, str>), + Inline(&'a Vec), +} + +impl<'a> Display for LinkLabel<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + LinkLabel::Text(s) => f.write_str(s), + LinkLabel::Inline(inlines) => f.write_str(&inlines_to_string(*inlines)), + } + } +} + +impl From for LinkTransformer { + fn from(value: LinkTransform) -> Self { + let delegate = match value { + LinkTransform::Keep => LinkTransformState::Keep, + LinkTransform::Inline => LinkTransformState::Inline, + LinkTransform::Reference => LinkTransformState::Reference(ReferenceAssigner::new()), + }; + Self { delegate } + } +} + +enum LinkTransformState { + Keep, + Inline, + Reference(ReferenceAssigner), +} + +impl LinkTransformer { + pub fn transform<'a>(&mut self, link: &'a LinkReference, label: &LinkLabel<'a>) -> Cow<'a, LinkReference> { + match &mut self.delegate { + LinkTransformState::Keep => Cow::Borrowed(&link), + LinkTransformState::Inline => Cow::Owned(LinkReference::Inline), + LinkTransformState::Reference(assigner) => assigner.assign(link, label), + } + } +} + +struct ReferenceAssigner { + /// Let's not worry about overflow. The minimum size for each link is 5 bytes (`[][1]`), so u64 of those is about + /// 80 exabytes of markdown -- and it would be even bigger than that, since the digits get bigger. It's just not a + /// case I'm too worried about right now. + next_index: u64, + /// Mappings from old reference id to new. We store these as an Strings (not ints) so that we don't need to worry + /// about overflow. + reorderings: HashMap, +} + +impl ReferenceAssigner { + fn new() -> Self { + Self { + next_index: 1, + reorderings: HashMap::with_capacity(16), // arbitrary + } + } + + fn assign<'a>(&mut self, link: &'a LinkReference, label: &LinkLabel<'a>) -> Cow<'a, LinkReference> { + match &link { + LinkReference::Inline => self.assign_new(), + LinkReference::Full(prev) => self.assign_if_numeric(prev).unwrap_or_else(|| Cow::Borrowed(link)), + LinkReference::Collapsed | LinkReference::Shortcut => { + let text_cow = match label { + LinkLabel::Text(text) => Cow::from(text.deref()), + LinkLabel::Inline(text) => Cow::Owned(inlines_to_string(text)), + }; + self.assign_if_numeric(text_cow.deref()).unwrap_or_else(|| { + let ref_text_owned = String::from(text_cow.deref()); + Cow::Owned(LinkReference::Full(ref_text_owned)) + }) + } + } + } + + fn assign_if_numeric<'a>(&mut self, prev: &str) -> Option> { + if prev.chars().all(|ch| ch.is_numeric()) { + match self.reorderings.entry(String::from(prev)) { + Entry::Occupied(map_to) => Some(Cow::Owned(LinkReference::Full(map_to.get().to_string()))), + Entry::Vacant(e) => { + e.insert(self.next_index); + Some(self.assign_new()) + } + } + } else { + None + } + } + + fn assign_new<'a>(&mut self) -> Cow<'a, LinkReference> { + let idx_str = self.next_index.to_string(); + self.next_index += 1; + Cow::Owned(LinkReference::Full(idx_str)) + } +} + +/// Turns the inlines into a String. Unlike [crate::fmt_str::inlines_to_plain_string], this respects formatting spans +/// like emphasis, strong, etc. +fn inlines_to_string(inlines: &Vec) -> String { + // see: https://github.com/yshavit/mdq/issues/87 + let mut string_writer = Output::new(String::with_capacity(32)); // guess at capacity + write_md( + &MdOptions { + link_reference_placement: ReferencePlacement::Section, + footnote_reference_placement: ReferencePlacement::Section, + link_canonicalization: LinkTransform::Keep, + add_thematic_breaks: false, + }, + &mut string_writer, + inlines.iter().map(|inline| MdElemRef::Inline(inline)), + ); + string_writer + .take_underlying() + .expect("internal error while parsing collapsed- or shortcut-style link") +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::utils_for_test::*; + use crate::variants_checker; + + enum Combo { + Of(LinkTransform, LinkReference), + } + + variants_checker!(VARIANTS_CHECKER = Combo { + Of(LinkTransform::Keep, LinkReference::Inline), + Of(LinkTransform::Keep, LinkReference::Collapsed), + Of(LinkTransform::Keep, LinkReference::Full(_)), + Of(LinkTransform::Keep, LinkReference::Shortcut), + + Of(LinkTransform::Inline, LinkReference::Shortcut), + Of(LinkTransform::Inline, LinkReference::Collapsed), + Of(LinkTransform::Inline, LinkReference::Full(_)), + Of(LinkTransform::Inline, LinkReference::Inline), + + Of(LinkTransform::Reference, LinkReference::Collapsed), + Of(LinkTransform::Reference, LinkReference::Full(_)), + Of(LinkTransform::Reference, LinkReference::Inline), + Of(LinkTransform::Reference, LinkReference::Shortcut), + }); + + mod keep { + use super::*; + + #[test] + fn inline() { + check_keep(&LinkReference::Inline); + } + + #[test] + fn collapsed() { + check_keep(&LinkReference::Collapsed); + } + + #[test] + fn full() { + check_keep(&LinkReference::Full("5".to_string())); + } + + #[test] + fn shortcut() { + check_keep(&LinkReference::Shortcut); + } + + fn check_keep(link_ref: &LinkReference) { + check_one( + LinkTransform::Keep, + link_ref, + &LinkLabel::Text(Cow::Borrowed("text label")), + &Cow::Borrowed(link_ref), + ); + } + } + + mod inline { + use super::*; + + #[test] + fn inline() { + // We could in principle have this return a Borrowed Cow, since the input and output are both Inline. + // But it's not really worth it, given that Inline is just a stateless enum variant and thus as cheap + // (or potentially even cheaper!) than a pointer. + check_inline(&LinkReference::Inline); + } + + #[test] + fn collapsed() { + check_inline(&LinkReference::Collapsed); + } + + #[test] + fn full() { + check_inline(&LinkReference::Full("5".to_string())); + } + + #[test] + fn shortcut() { + check_inline(&LinkReference::Shortcut); + } + + fn check_inline(link_ref: &LinkReference) { + check_one( + LinkTransform::Inline, + link_ref, + &LinkLabel::Text(Cow::Borrowed("text label")), + &Cow::Owned(LinkReference::Inline), + ); + } + } + + mod reference { + use super::*; + use crate::tree::{Formatting, FormattingVariant, Text, TextVariant}; + + #[test] + fn inline() { + check_one( + LinkTransform::Reference, + &LinkReference::Inline, + &LinkLabel::Text(Cow::Borrowed("doesn't matter")), + &Cow::Owned(LinkReference::Full("1".to_string())), + ) + } + + #[test] + fn collapsed_label_not_number() { + check_one( + LinkTransform::Reference, + &LinkReference::Collapsed, + &LinkLabel::Text(Cow::Borrowed("not a number")), + &Cow::Owned(LinkReference::Full("not a number".to_string())), + ) + } + + #[test] + fn collapsed_label_is_number() { + check_one( + LinkTransform::Reference, + &LinkReference::Collapsed, + &LinkLabel::Text(Cow::Borrowed("5")), + &Cow::Owned(LinkReference::Full("1".to_string())), // always count from 1 + ) + } + + #[test] + fn full_ref_id_not_number() { + let reference = LinkReference::Full("non-number".to_string()); + let reference_cow: Cow = Cow::Borrowed(&reference); + + check_one( + LinkTransform::Reference, + &reference, + &LinkLabel::Text(Cow::Borrowed("doesn't matter")), + &reference_cow, + ) + } + + #[test] + fn full_ref_id_is_number() { + check_one( + LinkTransform::Reference, + &LinkReference::Full("321".to_string()), // what number it is doesn't matter + &LinkLabel::Text(Cow::Borrowed("doesn't matter")), + &Cow::Owned(LinkReference::Full("1".to_string())), + ) + } + + #[test] + fn full_ref_id_is_huge_number() { + let huge_num_str = format!("{}00000", u128::MAX); + check_one( + LinkTransform::Reference, + &LinkReference::Full(huge_num_str), // what number it is doesn't matter + &LinkLabel::Text(Cow::Borrowed("doesn't matter")), + &Cow::Owned(LinkReference::Full("1".to_string())), // always count from 1 + ) + } + + #[test] + fn shortcut_label_not_number() { + check_one( + LinkTransform::Reference, + &LinkReference::Shortcut, + &LinkLabel::Text(Cow::Borrowed("not a number")), + &Cow::Owned(LinkReference::Full("not a number".to_string())), + ) + } + + #[test] + fn shortcut_label_is_number() { + check_one( + LinkTransform::Reference, + &LinkReference::Shortcut, + &LinkLabel::Text(Cow::Borrowed("5")), + &Cow::Owned(LinkReference::Full("1".to_string())), // always count from 1 + ) + } + + /// The label isn't even close to a number. + /// + /// _c.f._ [shortcut_label_inlines_are_emphasized_number] + #[test] + fn shortcut_label_inlines_not_number_like() { + check_one( + LinkTransform::Reference, + &LinkReference::Shortcut, + &LinkLabel::Inline(&vec![Inline::Text(Text { + variant: TextVariant::Plain, + value: "hello world".to_string(), + })]), + &Cow::Owned(LinkReference::Full("hello world".to_string())), + ) + } + + /// The label is kind of like a number, except that it's emphasized: `_123_`. This makes it not a number. + /// + /// _c.f._ [shortcut_label_inlines_not_number_like] + #[test] + fn shortcut_label_inlines_are_emphasized_number() { + check_one( + LinkTransform::Reference, + &LinkReference::Shortcut, + &LinkLabel::Inline(&vec![Inline::Formatting(Formatting { + variant: FormattingVariant::Emphasis, + children: vec![Inline::Text(Text { + variant: TextVariant::Plain, + value: "123".to_string(), + })], + })]), + &Cow::Owned(LinkReference::Full("_123_".to_string())), // note: the emphasis makes it not a number! + ) + } + + #[test] + fn shortcut_label_inlines_are_number() { + check_one( + LinkTransform::Reference, + &LinkReference::Shortcut, + &LinkLabel::Inline(&vec![Inline::Text(Text { + variant: TextVariant::Plain, + value: "123".to_string(), + })]), + &Cow::Owned(LinkReference::Full("1".to_string())), + ) + } + } + + /// A smoke test basically to ensure that we increment values correctly. We won't test every transformation type, + /// since the sibling sub-modules already do that. + #[test] + fn smoke_test_multi() { + let mut transformer = LinkTransformer::from(LinkTransform::Reference); + + // [alpha](https://example.com) ==> [alpha][1] + assert_eq_cow( + &transformer.transform(&LinkReference::Inline, &LinkLabel::Text(Cow::Borrowed("alpha"))), + &Cow::Owned(LinkReference::Full("1".to_string())), + ); + + // [bravo][1] ==> [bravo][2] + assert_eq_cow( + &transformer.transform( + &LinkReference::Full("1".to_string()), + &LinkLabel::Text(Cow::Borrowed("bravo")), + ), + &Cow::Owned(LinkReference::Full("2".to_string())), + ); + + // [charlie][3] ==> [charlie][3] + // Note that in this case, we could return a Borrowed cow, but we return a new Owned one anyway for simplicity + assert_eq_cow( + &transformer.transform( + &LinkReference::Full("3".to_string()), + &LinkLabel::Text(Cow::Borrowed("charlie")), + ), + &Cow::Owned(LinkReference::Full("3".to_string())), + ); + + // [delta][] ==> [delta][delta] + // Note that in this case, we could return a Borrowed cow, but we return a new Owned one anyway for simplicity + assert_eq_cow( + &transformer.transform(&LinkReference::Collapsed, &LinkLabel::Text(Cow::Borrowed("delta"))), + &Cow::Owned(LinkReference::Full("delta".to_string())), + ); + } + + fn check_one(transform: LinkTransform, link_ref: &LinkReference, label: &LinkLabel, expected: &Cow) { + let mut transformer = LinkTransformer::from(transform); + let actual = transformer.transform(&link_ref, &label); + + VARIANTS_CHECKER.see(&Combo::Of(transform, link_ref.clone())); + + assert_eq_cow(&actual, &expected); + } +} diff --git a/src/main.rs b/src/main.rs index 4faf326..b955d76 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ use std::io::{stdin, Read}; use std::process::ExitCode; use crate::fmt_md::{MdOptions, ReferencePlacement}; +use crate::link_transform::LinkTransform; use crate::output::Stream; use crate::select::ParseError; use crate::tree::{MdElem, ReadOptions}; @@ -12,6 +13,7 @@ use select::MdqRefSelector; mod fmt_md; mod fmt_str; +mod link_transform; mod matcher; mod output; mod parse_common; @@ -37,6 +39,9 @@ struct Cli { #[arg(long, value_enum)] footnote_pos: Option, + #[arg(long, short, value_enum, default_value_t=LinkTransform::Reference)] + link_canonicalization: LinkTransform, + /// The selector string selectors: Option, } @@ -65,9 +70,12 @@ fn main() -> ExitCode { pipeline_nodes = new_pipeline; } - let mut md_options = MdOptions::default(); - md_options.link_reference_placement = cli.link_pos; - md_options.footnote_reference_placement = cli.footnote_pos.unwrap_or(md_options.link_reference_placement); + let md_options = MdOptions { + link_reference_placement: cli.link_pos, + footnote_reference_placement: cli.footnote_pos.unwrap_or(cli.link_pos), + link_canonicalization: cli.link_canonicalization, + add_thematic_breaks: true, + }; fmt_md::write_md(&md_options, &mut out, pipeline_nodes.into_iter()); out.write_str("\n"); diff --git a/src/utils_for_test.rs b/src/utils_for_test.rs index a76b43f..fc99473 100644 --- a/src/utils_for_test.rs +++ b/src/utils_for_test.rs @@ -1,8 +1,12 @@ +#[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::borrow::Cow; + use std::fmt::Debug; /// Turn a pattern match into an `if let ... { else panic! }`. #[macro_export] macro_rules! unwrap { @@ -18,14 +22,14 @@ mod test_utils { /// Creates a static object named `$name` that looks for all the variants of enum `E`. /// /// ``` - /// variants_checker(CHECKER_NAME = MyEnum: { Variant1, Variant2(_), ... }) + /// variants_checker(CHECKER_NAME = MyEnum { Variant1, Variant2(_), ... }) /// ``` /// /// You can also mark some variants as ignored; these will be added to the pattern match, but not be required to /// be seen: /// /// ``` - /// variants_checker(CHECKER_NAME = MyEnum: { Variant1, ... } ignore { Variant2, ... } ) + /// variants_checker(CHECKER_NAME = MyEnum { Variant1, ... } ignore { Variant2, ... } ) /// ``` /// /// If you see a compilation failure here, it means the call site is missing variants (or has an unknown @@ -47,7 +51,7 @@ mod test_utils { } impl [] { - pub fn see(&self, node: &$enum_type) { + fn see(&self, node: &$enum_type) { let node_str = match node { $($enum_type::$variant => stringify!($variant),)* $($($enum_type::$ignore_variant => { @@ -57,7 +61,7 @@ mod test_utils { self.require.lock().map(|mut set| set.remove(node_str)).unwrap(); } - pub fn wait_for_all(&self) { + fn wait_for_all(&self) { use std::{thread, time}; let timeout = time::Duration::from_millis(500); @@ -102,4 +106,16 @@ mod test_utils { } }; } + + pub fn assert_eq_cow(actual: &Cow, expected: &Cow) + where + T: Clone + PartialEq + Debug, + { + assert_eq!(actual, expected); + match (actual, expected) { + (Cow::Borrowed(_), Cow::Owned(_)) => panic!("expected Cow::Owned({:?}) but saw Borrowed", expected), + (Cow::Owned(_), Cow::Borrowed(_)) => panic!("expected Cow::Borrowed({:?}) but saw Owned", expected), + (Cow::Borrowed(_), Cow::Borrowed(_)) | (Cow::Owned(_), Cow::Owned(_)) => {} + } + } }