Skip to main content

mdwright_format/format/
semantic.rs

1//! Semantic-equivalence comparison for formatter verification.
2//!
3//! The formatter owns the policy question ("did this rewrite preserve
4//! meaning?"). The document crate owns the parser mechanics used to
5//! produce comparable Markdown signatures.
6
7use mdwright_document::{ParseError, ParseOptions, markdown_signature};
8
9/// True iff the two Markdown sources parse to the same canonical
10/// document signature under default recognition policy.
11///
12/// # Errors
13///
14/// Returns [`ParseError`] if either input cannot be parsed safely.
15pub fn semantically_equivalent(source: &str, formatted: &str) -> Result<bool, ParseError> {
16    semantically_equivalent_with_options(source, formatted, ParseOptions::default())
17}
18
19pub(crate) fn semantically_equivalent_with_options(
20    source: &str,
21    formatted: &str,
22    parse_options: ParseOptions,
23) -> Result<bool, ParseError> {
24    Ok(markdown_signature(source, parse_options)? == markdown_signature(formatted, parse_options)?)
25}
26
27/// If `source` and `formatted` are not semantically equivalent,
28/// return a short human-readable description of the first divergent
29/// event pair. Returns `None` if the streams agree.
30///
31/// # Errors
32///
33/// Returns [`ParseError`] if either input cannot be parsed safely.
34pub fn first_divergence(source: &str, formatted: &str) -> Result<Option<String>, ParseError> {
35    first_divergence_with_options(source, formatted, ParseOptions::default())
36}
37
38pub(crate) fn first_divergence_with_options(
39    source: &str,
40    formatted: &str,
41    parse_options: ParseOptions,
42) -> Result<Option<String>, ParseError> {
43    let source_sig = markdown_signature(source, parse_options)?;
44    let formatted_sig = markdown_signature(formatted, parse_options)?;
45    Ok(source_sig.first_divergence(&formatted_sig))
46}
47
48#[cfg(test)]
49#[allow(clippy::expect_used)]
50mod tests {
51    use super::semantically_equivalent;
52
53    #[test]
54    fn prose_rewrap_inside_paragraph_is_equivalent() {
55        let a = "alpha beta gamma\ndelta epsilon zeta\n";
56        let b = "alpha beta gamma delta\nepsilon zeta\n";
57        assert!(semantically_equivalent(a, b).expect("semantic check parses"));
58    }
59
60    #[test]
61    fn prose_rewrap_inside_blockquote_is_equivalent() {
62        let a = "> alpha beta gamma\n> delta epsilon zeta\n";
63        let b = "> alpha beta gamma delta epsilon\n> zeta\n";
64        assert!(semantically_equivalent(a, b).expect("semantic check parses"));
65    }
66
67    #[test]
68    fn whitespace_change_inside_fenced_code_is_rejected() {
69        let a = "```\nfoo\nbar\n```\n";
70        let b = "```\nfoo bar\n```\n";
71        assert!(!semantically_equivalent(a, b).expect("semantic check parses"));
72    }
73
74    #[test]
75    fn whitespace_change_inside_inline_code_is_rejected() {
76        let a = "see `x  y` here\n";
77        let b = "see `x y` here\n";
78        assert!(!semantically_equivalent(a, b).expect("semantic check parses"));
79    }
80
81    #[test]
82    fn dropped_emphasis_is_rejected() {
83        let a = "foo *bar* baz\n";
84        let b = "foo bar baz\n";
85        assert!(!semantically_equivalent(a, b).expect("semantic check parses"));
86    }
87
88    #[test]
89    fn link_target_change_is_rejected() {
90        let a = "[label](https://a.example)\n";
91        let b = "[label](https://b.example)\n";
92        assert!(!semantically_equivalent(a, b).expect("semantic check parses"));
93    }
94
95    #[test]
96    fn link_text_rewrap_is_equivalent() {
97        let a = "[label one\ntwo](https://x.example)\n";
98        let b = "[label one two](https://x.example)\n";
99        assert!(semantically_equivalent(a, b).expect("semantic check parses"));
100    }
101
102    #[test]
103    fn table_cell_whitespace_rewrap_is_equivalent() {
104        let a = "| a | b |\n|---|---|\n| x | y |\n";
105        let b = "| a   | b   |\n| --- | --- |\n| x   | y   |\n";
106        assert!(semantically_equivalent(a, b).expect("semantic check parses"));
107    }
108
109    #[test]
110    fn dropped_heading_level_is_rejected() {
111        let a = "## foo\n";
112        let b = "### foo\n";
113        assert!(!semantically_equivalent(a, b).expect("semantic check parses"));
114    }
115}