markdown_code_runner/
markers.rs

1//! Marker constants and regex patterns for markdown-code-runner.
2
3use once_cell::sync::Lazy;
4use regex::Regex;
5
6/// Format a string as a Markdown comment.
7pub fn md_comment(text: &str) -> String {
8    format!("<!-- {} -->", text)
9}
10
11/// Warning comment inserted in output sections.
12pub const WARNING: &str =
13    "<!-- \u{26A0}\u{FE0F} This content is auto-generated by `markdown-code-runner`. -->";
14
15/// Skip marker to prevent code execution.
16pub const SKIP: &str = "<!-- CODE:SKIP -->";
17
18/// Python code comment start marker.
19pub const CODE_COMMENT_PYTHON_START: &str = "<!-- CODE:START -->";
20
21/// Bash code comment start marker.
22pub const CODE_COMMENT_BASH_START: &str = "<!-- CODE:BASH:START -->";
23
24/// Code comment end marker.
25pub const CODE_COMMENT_END: &str = "<!-- CODE:END -->";
26
27/// Output start marker.
28pub const OUTPUT_START: &str = "<!-- OUTPUT:START -->";
29
30/// Output end marker.
31pub const OUTPUT_END: &str = "<!-- OUTPUT:END -->";
32
33// Compiled regex patterns
34
35/// Pattern to match skip marker with optional leading whitespace.
36pub static SKIP_PATTERN: Lazy<Regex> =
37    Lazy::new(|| Regex::new(r"^(?P<spaces>\s*)<!-- CODE:SKIP -->").unwrap());
38
39/// Pattern to match Python code comment start with optional leading whitespace.
40pub static CODE_COMMENT_PYTHON_START_PATTERN: Lazy<Regex> =
41    Lazy::new(|| Regex::new(r"^(?P<spaces>\s*)<!-- CODE:START -->").unwrap());
42
43/// Pattern to match Bash code comment start with optional leading whitespace.
44pub static CODE_COMMENT_BASH_START_PATTERN: Lazy<Regex> =
45    Lazy::new(|| Regex::new(r"^(?P<spaces>\s*)<!-- CODE:BASH:START -->").unwrap());
46
47/// Pattern to match code comment end with optional leading whitespace.
48pub static CODE_COMMENT_END_PATTERN: Lazy<Regex> =
49    Lazy::new(|| Regex::new(r"^(?P<spaces>\s*)<!-- CODE:END -->").unwrap());
50
51/// Pattern to match output start with optional leading whitespace.
52pub static OUTPUT_START_PATTERN: Lazy<Regex> =
53    Lazy::new(|| Regex::new(r"^(?P<spaces>\s*)<!-- OUTPUT:START -->").unwrap());
54
55/// Pattern to match output end with optional leading whitespace.
56pub static OUTPUT_END_PATTERN: Lazy<Regex> =
57    Lazy::new(|| Regex::new(r"^(?P<spaces>\s*)<!-- OUTPUT:END -->").unwrap());
58
59/// Pattern to match backtick code block start with markdown-code-runner.
60pub static CODE_BACKTICKS_START_PATTERN: Lazy<Regex> = Lazy::new(|| {
61    Regex::new(r"^(?P<spaces>\s*)```(?P<language>\w+)\s+markdown-code-runner").unwrap()
62});
63
64/// Pattern to match backtick code block end.
65pub static CODE_BACKTICKS_END_PATTERN: Lazy<Regex> =
66    Lazy::new(|| Regex::new(r"^(?P<spaces>\s*)```\s*$").unwrap());
67
68/// Check if a line matches the skip marker.
69pub fn is_skip(line: &str) -> bool {
70    SKIP_PATTERN.is_match(line)
71}
72
73/// Check if a line matches the output start marker.
74pub fn is_output_start(line: &str) -> Option<regex::Match<'_>> {
75    OUTPUT_START_PATTERN.find(line)
76}
77
78/// Check if a line matches the output end marker.
79pub fn is_output_end(line: &str) -> bool {
80    OUTPUT_END_PATTERN.is_match(line)
81}
82
83/// Check if a line matches the Python code comment start marker.
84pub fn is_code_comment_python_start(line: &str) -> Option<regex::Match<'_>> {
85    CODE_COMMENT_PYTHON_START_PATTERN.find(line)
86}
87
88/// Check if a line matches the Bash code comment start marker.
89pub fn is_code_comment_bash_start(line: &str) -> Option<regex::Match<'_>> {
90    CODE_COMMENT_BASH_START_PATTERN.find(line)
91}
92
93/// Check if a line matches the code comment end marker.
94pub fn is_code_comment_end(line: &str) -> bool {
95    CODE_COMMENT_END_PATTERN.is_match(line)
96}
97
98/// Check if a line matches the backticks code block start marker.
99pub fn is_code_backticks_start(line: &str) -> Option<regex::Captures<'_>> {
100    CODE_BACKTICKS_START_PATTERN.captures(line)
101}
102
103/// Check if a line matches the backticks code block end marker.
104pub fn is_code_backticks_end(line: &str) -> bool {
105    CODE_BACKTICKS_END_PATTERN.is_match(line)
106}
107
108/// Remove Markdown comment tags from a string.
109/// Returns None if the string is not a valid Markdown comment.
110pub fn remove_md_comment(line: &str) -> Option<String> {
111    let trimmed = line.trim();
112    if trimmed.starts_with("<!-- ") && trimmed.ends_with(" -->") {
113        Some(trimmed[5..trimmed.len() - 4].to_string())
114    } else {
115        None
116    }
117}
118
119/// Extract leading whitespace from a line.
120pub fn get_indent(line: &str) -> String {
121    let trimmed = line.trim_start();
122    line[..line.len() - trimmed.len()].to_string()
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn test_md_comment() {
131        assert_eq!(md_comment("test"), "<!-- test -->");
132    }
133
134    #[test]
135    fn test_remove_md_comment() {
136        assert_eq!(
137            remove_md_comment("<!-- This is a comment -->"),
138            Some("This is a comment".to_string())
139        );
140        assert_eq!(remove_md_comment("This is not a comment"), None);
141    }
142
143    #[test]
144    fn test_is_skip() {
145        assert!(is_skip("<!-- CODE:SKIP -->"));
146        assert!(is_skip("  <!-- CODE:SKIP -->"));
147        assert!(!is_skip("some text"));
148    }
149
150    #[test]
151    fn test_get_indent() {
152        assert_eq!(get_indent("    hello"), "    ");
153        assert_eq!(get_indent("hello"), "");
154        assert_eq!(get_indent("\t\thello"), "\t\t");
155    }
156
157    #[test]
158    fn test_is_code_backticks_start() {
159        let caps = is_code_backticks_start("```python markdown-code-runner");
160        assert!(caps.is_some());
161        let caps = caps.unwrap();
162        assert_eq!(&caps["language"], "python");
163
164        let caps = is_code_backticks_start("    ```bash markdown-code-runner filename=test.sh");
165        assert!(caps.is_some());
166        let caps = caps.unwrap();
167        assert_eq!(&caps["language"], "bash");
168        assert_eq!(&caps["spaces"], "    ");
169
170        assert!(is_code_backticks_start("```python").is_none());
171    }
172}