rumdl_lib/rules/
code_fence_utils.rs1use std::fmt;
2
3pub(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 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#[derive(Debug, PartialEq, Eq, Clone, Copy, Default, Hash)]
50pub enum CodeFenceStyle {
51 #[default]
53 Consistent,
54 Backtick,
56 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}