Skip to main content

rumdl_lib/rules/
code_fence_utils.rs

1use std::fmt;
2
3/// A literal closer makes the rendered roles of this fence and later markers
4/// unknowable without evaluating the template. Keep earlier independent fences
5/// available, but do not infer or fix fence structure from this opener onward.
6pub(crate) fn fence_ambiguity_limit(ctx: &crate::lint_context::LintContext) -> Option<usize> {
7    let lines = ctx.raw_lines();
8    ctx.code_block_details.iter().find_map(|detail| {
9        if !detail.is_fenced || ctx.is_inside_template_code(detail.start) || ctx.is_inside_mdx_code(detail.start) {
10            return None;
11        }
12        let start_line = ctx
13            .line_offsets
14            .partition_point(|&off| off <= detail.start)
15            .saturating_sub(1);
16        let end_line = ctx
17            .line_offsets
18            .partition_point(|&off| off <= detail.end.saturating_sub(1))
19            .saturating_sub(1);
20        if start_line == end_line {
21            return None;
22        }
23        let opener = lines.get(start_line)?.as_bytes();
24        let open_start = opener.iter().position(|&b| b == b'`' || b == b'~')?;
25        let fence_char = opener[open_start];
26        let open_len = opener[open_start..].iter().take_while(|&&b| b == fence_char).count();
27        let closer = lines.get(end_line)?;
28        let close_start = closer.bytes().position(|b| b == b'`' || b == b'~')?;
29        let close_len = closer.as_bytes()[close_start..]
30            .iter()
31            .take_while(|&&b| b == fence_char)
32            .count();
33        // A cached unclosed block also ends at EOF. Only a matching bare fence
34        // can establish this ambiguity, rather than arbitrary final body text.
35        if open_len < 3
36            || close_len < open_len
37            || !closer[..close_start].chars().all(|c| c.is_whitespace() || c == '>')
38            || !closer[close_start + close_len..].chars().all(char::is_whitespace)
39        {
40            return None;
41        }
42        let close_offset = ctx.line_offsets[end_line] + close_start;
43        (ctx.is_inside_template_code(close_offset) || ctx.is_inside_mdx_code(close_offset))
44            .then_some(ctx.line_offsets[start_line])
45    })
46}
47
48/// The style for code fence markers (MD048)
49#[derive(Debug, PartialEq, Eq, Clone, Copy, Default, Hash)]
50pub enum CodeFenceStyle {
51    /// Consistent with the first code fence style found
52    #[default]
53    Consistent,
54    /// Backtick style (```)
55    Backtick,
56    /// Tilde style (~~~)
57    Tilde,
58}
59
60impl fmt::Display for CodeFenceStyle {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        match self {
63            CodeFenceStyle::Backtick => write!(f, "backtick"),
64            CodeFenceStyle::Tilde => write!(f, "tilde"),
65            CodeFenceStyle::Consistent => write!(f, "consistent"),
66        }
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn test_fence_ambiguity_requires_a_literal_matching_closer() {
76        use crate::config::MarkdownFlavor;
77        use crate::lint_context::LintContext;
78        let prefix = "~~~rust\nSafe\n~~~\n\n";
79        for ending in ["\n", "\r\n"] {
80            for marker in ["~~~", "~~~~", "  ~~~"] {
81                let code = format!("Visible {{{{ \"first\n{marker}\nlast\" }}}}");
82                let source = format!("{prefix}~~~rust\n{code}\n~~~\nAfter.\n").replace('\n', ending);
83                let ctx = LintContext::new(&source, MarkdownFlavor::Standard, None);
84                assert_eq!(
85                    fence_ambiguity_limit(&ctx),
86                    Some(prefix.replace('\n', ending).len()),
87                    "{source}"
88                );
89            }
90        }
91        for (opener, marker, tail) in [
92            ("~~~rust", "~~~ trailing", "last\" }}\n~~~\n"),
93            ("~~~rust", "```", "last\" }}\n~~~\n"),
94            ("~~~~rust", "~~~", "last\" }}\n~~~~\n"),
95            ("~~~rust", "~~~ trailing", ""),
96        ] {
97            let source = format!("{opener}\nVisible {{{{ \"first\n{marker}\n{tail}");
98            let ctx = LintContext::new(&source, MarkdownFlavor::Standard, None);
99            assert_eq!(fence_ambiguity_limit(&ctx), None, "{source}");
100        }
101    }
102
103    #[test]
104    fn test_code_fence_style_default() {
105        let style = CodeFenceStyle::default();
106        assert_eq!(style, CodeFenceStyle::Consistent);
107    }
108
109    #[test]
110    fn test_code_fence_style_display() {
111        assert_eq!(format!("{}", CodeFenceStyle::Backtick), "backtick");
112        assert_eq!(format!("{}", CodeFenceStyle::Tilde), "tilde");
113        assert_eq!(format!("{}", CodeFenceStyle::Consistent), "consistent");
114    }
115}