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::{CodeBlockMatcher, Select, TrySelector};

#[derive(Debug, PartialEq)]
pub(crate) struct CodeBlockSelector {
    lang_matcher: StringMatcher,
    contents_matcher: StringMatcher,
}

impl From<CodeBlockMatcher> for CodeBlockSelector {
    fn from(value: CodeBlockMatcher) -> Self {
        Self {
            lang_matcher: value.language.into(),
            contents_matcher: value.contents.into(),
        }
    }
}

impl TrySelector<CodeBlock> for CodeBlockSelector {
    fn try_select(&self, _: &MdContext, item: CodeBlock) -> crate::select::Result<Select> {
        let (has_code_opts, language, metadata) = match item.variant {
            CodeVariant::Code(None) => (false, String::new(), None),
            CodeVariant::Code(Some(CodeOpts { language, metadata })) => (true, language, metadata),
            CodeVariant::Math { .. } => {
                return Ok(Select::Miss(item.into()));
            }
        };
        let lang_sel = self
            .lang_matcher
            .match_replace_string(language)
            .map_err(|e| e.to_select_error("code block"))?;
        let content_sel = self
            .contents_matcher
            .match_replace_string(item.value)
            .map_err(|e| e.to_select_error("code block"))?;

        let item_is_match = lang_sel.matched_any && content_sel.matched_any;

        let selected_code_opts = if has_code_opts {
            Some(CodeOpts {
                language: lang_sel.item,
                metadata,
            })
        } else {
            None
        };

        let selected_block = CodeBlock {
            variant: CodeVariant::Code(selected_code_opts),
            value: content_sel.item,
        };
        Ok(make_select_result(selected_block, item_is_match))
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::md_elem::{MdContext, MdElem};
    use crate::select::{MatchReplace, Select, TrySelector};

    #[test]
    fn both_match() {
        let code_block = new_code_block("main()", Some("rust"));

        let result = CodeBlockSelector::from(CodeBlockMatcher {
            language: MatchReplace::match_any(),
            contents: MatchReplace::match_any(),
        })
        .try_select(&MdContext::default(), code_block.clone())
        .unwrap();

        assert_eq!(result, Select::Hit(vec![MdElem::CodeBlock(code_block)]));
    }

    #[test]
    fn language_match_hits() {
        let code_block = new_code_block("main()", Some("rust"));

        let result = CodeBlockSelector::from(CodeBlockMatcher {
            language: MatchReplace::build(|b| b.match_regex("rust")),
            contents: MatchReplace::match_any(),
        })
        .try_select(&MdContext::default(), code_block.clone())
        .unwrap();

        assert_eq!(result, Select::Hit(vec![MdElem::CodeBlock(code_block)]));
    }

    #[test]
    fn language_match_misses() {
        let code_block = new_code_block("main", Some("rust"));

        let result = CodeBlockSelector::from(CodeBlockMatcher {
            language: MatchReplace::build(|b| b.match_regex("python")),
            contents: MatchReplace::match_any(),
        })
        .try_select(&MdContext::default(), code_block.clone())
        .unwrap();

        assert_eq!(result, Select::Miss(MdElem::CodeBlock(code_block)));
    }

    #[test]
    fn contents_match_hits() {
        let code_block = new_code_block("main()", Some("rust"));

        let result = CodeBlockSelector::from(CodeBlockMatcher {
            language: MatchReplace::match_any(),
            contents: MatchReplace::build(|b| b.match_regex("main")),
        })
        .try_select(&MdContext::default(), code_block.clone())
        .unwrap();

        assert_eq!(result, Select::Hit(vec![MdElem::CodeBlock(code_block)]));
    }

    #[test]
    fn contents_match_misses() {
        let code_block = new_code_block("main()", Some("rust"));

        let result = CodeBlockSelector::from(CodeBlockMatcher {
            language: MatchReplace::match_any(),
            contents: MatchReplace::build(|b| b.match_regex("println")),
        })
        .try_select(&MdContext::default(), code_block.clone())
        .unwrap();

        assert_eq!(result, Select::Miss(MdElem::CodeBlock(code_block)));
    }

    #[test]
    fn language_replacement_with_match() {
        let code_block = new_code_block("def main", Some("python"));

        let result = CodeBlockSelector::from(CodeBlockMatcher {
            language: MatchReplace::build(|b| b.match_regex("python").replacement("py")),
            contents: MatchReplace::match_any(),
        })
        .try_select(&MdContext::default(), code_block)
        .unwrap();

        assert_eq!(
            result,
            Select::Hit(vec![MdElem::CodeBlock(new_code_block("def main", Some("py")))])
        );
    }

    #[test]
    fn language_replacement_with_miss() {
        let code_block = new_code_block("main()", Some("rust"));

        let result = CodeBlockSelector::from(CodeBlockMatcher {
            language: MatchReplace::build(|b| b.match_regex("python").replacement("py")),
            contents: MatchReplace::match_any(),
        })
        .try_select(&MdContext::default(), code_block.clone())
        .unwrap();

        assert_eq!(result, Select::Miss(MdElem::CodeBlock(code_block)));
    }

    #[test]
    fn contents_replacement_with_match() {
        let code_block = new_code_block("main()", Some("rust"));

        let result = CodeBlockSelector::from(CodeBlockMatcher {
            language: MatchReplace::match_any(),
            contents: MatchReplace::build(|b| b.match_regex("main").replacement("start")),
        })
        .try_select(&MdContext::default(), code_block)
        .unwrap();

        assert_eq!(
            result,
            Select::Hit(vec![MdElem::CodeBlock(new_code_block("start()", Some("rust")))])
        );
    }

    #[test]
    fn contents_replacement_with_miss() {
        let code_block = new_code_block("main()", Some("rust"));

        let result = CodeBlockSelector::from(CodeBlockMatcher {
            language: MatchReplace::match_any(),
            contents: MatchReplace::build(|b| b.match_regex("println").replacement("print")),
        })
        .try_select(&MdContext::default(), code_block.clone())
        .unwrap();

        assert_eq!(result, Select::Miss(MdElem::CodeBlock(code_block)));
    }

    #[test]
    fn both_replacement_with_match() {
        let code_block = new_code_block("def main:", Some("python"));

        let result = CodeBlockSelector::from(CodeBlockMatcher {
            language: MatchReplace::build(|b| b.match_regex("python").replacement("py")),
            contents: MatchReplace::build(|b| b.match_regex("def (\\w+):").replacement("$1()")),
        })
        .try_select(&MdContext::default(), code_block)
        .unwrap();

        assert_eq!(
            result,
            Select::Hit(vec![MdElem::CodeBlock(new_code_block("main()", Some("py")))])
        );
    }

    #[test]
    fn code_language_is_explicitly_empty() {
        let code_block = new_code_block("text", Some(""));

        let result = CodeBlockSelector::from(CodeBlockMatcher {
            language: MatchReplace::match_any(),
            contents: MatchReplace::match_any(),
        })
        .try_select(&MdContext::default(), code_block.clone())
        .unwrap();

        assert_eq!(result, Select::Hit(vec![MdElem::CodeBlock(code_block)]));
    }

    #[test]
    fn no_language_code_but_match_includes_one() {
        let code_block = new_code_block("main()", None);

        let result = CodeBlockSelector::from(CodeBlockMatcher {
            language: MatchReplace::build(|b| b.match_regex("rust")),
            contents: MatchReplace::match_any(),
        })
        .try_select(&MdContext::default(), code_block.clone())
        .unwrap();

        assert_eq!(result, Select::Miss(MdElem::CodeBlock(code_block)));
    }

    #[test]
    fn no_language_code_and_match_doesnt_include_one() {
        let code_block = new_code_block("main()", None);

        let result = CodeBlockSelector::from(CodeBlockMatcher {
            language: MatchReplace::match_any(),
            contents: MatchReplace::match_any(),
        })
        .try_select(&MdContext::default(), code_block.clone())
        .unwrap();

        assert_eq!(result, Select::Hit(vec![MdElem::CodeBlock(code_block)]));
    }

    #[test]
    fn math_block_never_matches() {
        let math_block = CodeBlock {
            variant: CodeVariant::Math {
                metadata: Some("math".to_string()),
            },
            value: "x = 1".to_string(),
        };

        let result = CodeBlockSelector::from(CodeBlockMatcher {
            language: MatchReplace::match_any(),
            contents: MatchReplace::match_any(),
        })
        .try_select(&MdContext::default(), math_block.clone())
        .unwrap();

        assert_eq!(result, Select::Miss(MdElem::CodeBlock(math_block)));
    }

    fn new_code_block(content: &str, language: Option<&str>) -> CodeBlock {
        CodeBlock {
            variant: CodeVariant::Code(language.map(|lang| CodeOpts {
                language: lang.to_string(),
                metadata: None,
            })),
            value: content.to_string(),
        }
    }
}