mdq 0.10.0

Select and render specific elements in a Markdown document
Documentation
use crate::md_elem::elem::*;
use crate::md_elem::MdContext;
use crate::select::match_selector::make_select_result;
use crate::select::string_matcher::StringMatcher;
use crate::select::{LinklikeMatcher, Result, Select, TrySelector};

#[derive(Debug, PartialEq)]
pub(crate) struct LinkSelector {
    matchers: LinkMatchers,
}

impl From<LinklikeMatcher> for LinkSelector {
    fn from(value: LinklikeMatcher) -> Self {
        Self { matchers: value.into() }
    }
}

impl TrySelector<Link> for LinkSelector {
    fn try_select(&self, _: &MdContext, item: Link) -> Result<Select> {
        match item {
            Link::Standard(standard_link) => {
                let original_link = Link::Standard(standard_link.clone());

                let display_replaced = self
                    .matchers
                    .display_matcher
                    .match_replace_inlines(standard_link.display)
                    .map_err(|e| e.to_select_error("hyperlink"))?;
                let url_replaced = self
                    .matchers
                    .url_matcher
                    .match_replace_string(standard_link.link.url)
                    .map_err(|e| e.to_select_error("hyperlink"))?;

                // Both matchers must match for this to be a Hit
                let both_matched = display_replaced.matched_any && url_replaced.matched_any;

                if both_matched {
                    // Apply replacements
                    let result = Link::Standard(StandardLink {
                        display: display_replaced.item,
                        link: LinkDefinition {
                            url: url_replaced.item,
                            title: standard_link.link.title,
                            reference: standard_link.link.reference,
                        },
                    });
                    Ok(Select::Hit(vec![result.into()]))
                } else {
                    // Return original unchanged
                    Ok(Select::Miss(original_link.into()))
                }
            }
            Link::Autolink(autolink) => {
                let original_link = Link::Autolink(autolink.clone());

                let display_replaced = self
                    .matchers
                    .display_matcher
                    .match_replace_string(autolink.url.clone())
                    .map_err(|e| e.to_select_error("hyperlink"))?;
                let url_replaced = self
                    .matchers
                    .url_matcher
                    .match_replace_string(autolink.url)
                    .map_err(|e| e.to_select_error("hyperlink"))?;

                // Both matchers must match for this to be a Hit
                let both_matched = display_replaced.matched_any && url_replaced.matched_any;

                let result = if both_matched {
                    Link::Autolink(Autolink {
                        url: url_replaced.item,
                        style: autolink.style,
                    })
                } else {
                    original_link
                };
                Ok(make_select_result(result, both_matched))
            }
        }
    }
}

#[derive(Debug, PartialEq)]
pub(crate) struct ImageSelector {
    matchers: LinkMatchers,
}

impl From<LinklikeMatcher> for ImageSelector {
    fn from(value: LinklikeMatcher) -> Self {
        Self { matchers: value.into() }
    }
}

impl TrySelector<Image> for ImageSelector {
    fn try_select(&self, _: &MdContext, item: Image) -> Result<Select> {
        let original_image = item.clone();

        let alt_replaced = self
            .matchers
            .display_matcher
            .match_replace_string(item.alt)
            .map_err(|e| e.to_select_error("image"))?;
        let url_replaced = self
            .matchers
            .url_matcher
            .match_replace_string(item.link.url)
            .map_err(|e| e.to_select_error("image"))?;

        let both_matched = alt_replaced.matched_any && url_replaced.matched_any;

        let result = if both_matched {
            Image {
                alt: alt_replaced.item,
                link: LinkDefinition {
                    url: url_replaced.item,
                    title: item.link.title,
                    reference: item.link.reference,
                },
            }
        } else {
            original_image
        };
        Ok(make_select_result(result, both_matched))
    }
}

#[derive(Debug, PartialEq)]
pub(crate) struct LinkMatchers {
    pub(crate) display_matcher: StringMatcher,
    pub(crate) url_matcher: StringMatcher,
}

impl From<LinklikeMatcher> for LinkMatchers {
    fn from(value: LinklikeMatcher) -> Self {
        Self {
            display_matcher: value.display_matcher.into(),
            url_matcher: value.url_matcher.into(),
        }
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::md_elem::{mdq_inline, MdElem};
    use crate::select::MatchReplace;
    use crate::util::utils_for_test::unwrap;

    #[test]
    fn link_selector_url_replacement() {
        let link_matcher = LinklikeMatcher {
            display_matcher: MatchReplace::match_any(),
            url_matcher: MatchReplace::build(|b| b.match_regex("original.com").replacement("newsite.com")),
        };

        let link = Link::Standard(StandardLink {
            display: vec![],
            link: LinkDefinition {
                url: "https://original.com/path".to_string(),
                title: None,
                reference: LinkReference::Inline,
            },
        });

        let link_selector = LinkSelector::from(link_matcher);

        let result = link_selector.try_select(&MdContext::default(), link);
        unwrap!(result, Ok(Select::Hit(elems)));

        assert_eq!(elems.len(), 1);
        unwrap!(&elems[0], MdElem::Inline(Inline::Link(modified_link)));
        match modified_link {
            Link::Standard(standard_link) => assert_eq!(standard_link.link.url, "https://newsite.com/path"),
            Link::Autolink(..) => panic!("Expected Standard link, got Autolink"),
        }
    }

    #[test]
    fn image_selector_url_replacement() {
        let image_matcher = LinklikeMatcher {
            display_matcher: MatchReplace::match_any(),
            url_matcher: MatchReplace::build(|b| b.match_regex("old-image.png").replacement("new-image.png")),
        };

        let image = Image {
            alt: "alt text".to_string(),
            link: LinkDefinition {
                url: "https://example.com/old-image.png".to_string(),
                title: None,
                reference: LinkReference::Inline,
            },
        };

        let image_selector = ImageSelector::from(image_matcher);

        let result = image_selector.try_select(&MdContext::default(), image);
        unwrap!(result, Ok(Select::Hit(elems)));
        assert_eq!(elems.len(), 1);
        unwrap!(&elems[0], MdElem::Inline(Inline::Image(modified_image)));
        assert_eq!(modified_image.link.url, "https://example.com/new-image.png");
    }

    #[test]
    fn image_url_replaced_but_alt_does_not_match() {
        let image_matcher = LinklikeMatcher {
            display_matcher: MatchReplace::build(|b| b.match_regex("^wrong alt text$")),
            url_matcher: MatchReplace::build(|b| b.match_regex("old-image.png").replacement("new-image.png")),
        };

        let original_image = Image {
            alt: "original alt text".to_string(),
            link: LinkDefinition {
                url: "https://example.com/old-image.png".to_string(),
                title: None,
                reference: LinkReference::Inline,
            },
        };

        let image_selector = ImageSelector::from(image_matcher);

        let result = image_selector.try_select(&MdContext::default(), original_image.clone());
        unwrap!(result, Ok(Select::Miss(elem)));
        unwrap!(&elem, MdElem::Inline(Inline::Image(result_image)));

        assert_eq!(result_image, &original_image);
    }

    #[test]
    fn link_url_replaced_but_display_does_not_match() {
        let link_matcher = LinklikeMatcher {
            display_matcher: MatchReplace::build(|b| b.match_regex("^wrong display text$")),
            url_matcher: MatchReplace::build(|b| b.match_regex("original.com").replacement("newsite.com")),
        };

        let original_link = Link::Standard(StandardLink {
            display: vec![mdq_inline!("original display text")],
            link: LinkDefinition {
                url: "https://original.com/path".to_string(),
                title: None,
                reference: LinkReference::Inline,
            },
        });

        let link_selector = LinkSelector::from(link_matcher);

        let result = link_selector.try_select(&MdContext::default(), original_link.clone());
        unwrap!(result, Ok(Select::Miss(elem)));
        unwrap!(&elem, MdElem::Inline(Inline::Link(result_link)));

        assert_eq!(result_link, &original_link);
    }

    #[test]
    fn link_display_text_replacement() {
        let link_matcher = LinklikeMatcher {
            display_matcher: MatchReplace::build(|b| b.match_regex("old text").replacement("new text")),
            url_matcher: MatchReplace::match_any(),
        };

        let link = Link::Standard(StandardLink {
            display: vec![mdq_inline!("old text here")],
            link: LinkDefinition {
                url: "https://example.com".to_string(),
                title: None,
                reference: LinkReference::Inline,
            },
        });

        let link_selector = LinkSelector::from(link_matcher);

        let result = link_selector.try_select(&MdContext::default(), link);
        unwrap!(result, Ok(Select::Hit(elems)));

        assert_eq!(elems.len(), 1);
        unwrap!(&elems[0], MdElem::Inline(Inline::Link(Link::Standard(standard_link))));
        assert_eq!(standard_link.display, vec![mdq_inline!("new text here")]);
        assert_eq!(standard_link.link.url, "https://example.com");
    }

    #[test]
    fn image_alt_text_replacement() {
        let image_matcher = LinklikeMatcher {
            display_matcher: MatchReplace::build(|b| b.match_regex("old alt").replacement("new alt")),
            url_matcher: MatchReplace::match_any(),
        };

        let image = Image {
            alt: "old alt text".to_string(),
            link: LinkDefinition {
                url: "https://example.com/image.png".to_string(),
                title: None,
                reference: LinkReference::Inline,
            },
        };

        let image_selector = ImageSelector::from(image_matcher);

        let result = image_selector.try_select(&MdContext::default(), image);
        unwrap!(result, Ok(Select::Hit(elems)));
        assert_eq!(elems.len(), 1);
        unwrap!(&elems[0], MdElem::Inline(Inline::Image(modified_image)));
        assert_eq!(modified_image.alt, "new alt text");
        assert_eq!(modified_image.link.url, "https://example.com/image.png");
    }
}