markdown-code-runner 0.1.0

Automatically update Markdown files with code block output
Documentation
//! Marker constants and regex patterns for markdown-code-runner.

use once_cell::sync::Lazy;
use regex::Regex;

/// Format a string as a Markdown comment.
pub fn md_comment(text: &str) -> String {
    format!("<!-- {} -->", text)
}

/// Warning comment inserted in output sections.
pub const WARNING: &str =
    "<!-- \u{26A0}\u{FE0F} This content is auto-generated by `markdown-code-runner`. -->";

/// Skip marker to prevent code execution.
pub const SKIP: &str = "<!-- CODE:SKIP -->";

/// Python code comment start marker.
pub const CODE_COMMENT_PYTHON_START: &str = "<!-- CODE:START -->";

/// Bash code comment start marker.
pub const CODE_COMMENT_BASH_START: &str = "<!-- CODE:BASH:START -->";

/// Code comment end marker.
pub const CODE_COMMENT_END: &str = "<!-- CODE:END -->";

/// Output start marker.
pub const OUTPUT_START: &str = "<!-- OUTPUT:START -->";

/// Output end marker.
pub const OUTPUT_END: &str = "<!-- OUTPUT:END -->";

// Compiled regex patterns

/// Pattern to match skip marker with optional leading whitespace.
pub static SKIP_PATTERN: Lazy<Regex> =
    Lazy::new(|| Regex::new(r"^(?P<spaces>\s*)<!-- CODE:SKIP -->").unwrap());

/// Pattern to match Python code comment start with optional leading whitespace.
pub static CODE_COMMENT_PYTHON_START_PATTERN: Lazy<Regex> =
    Lazy::new(|| Regex::new(r"^(?P<spaces>\s*)<!-- CODE:START -->").unwrap());

/// Pattern to match Bash code comment start with optional leading whitespace.
pub static CODE_COMMENT_BASH_START_PATTERN: Lazy<Regex> =
    Lazy::new(|| Regex::new(r"^(?P<spaces>\s*)<!-- CODE:BASH:START -->").unwrap());

/// Pattern to match code comment end with optional leading whitespace.
pub static CODE_COMMENT_END_PATTERN: Lazy<Regex> =
    Lazy::new(|| Regex::new(r"^(?P<spaces>\s*)<!-- CODE:END -->").unwrap());

/// Pattern to match output start with optional leading whitespace.
pub static OUTPUT_START_PATTERN: Lazy<Regex> =
    Lazy::new(|| Regex::new(r"^(?P<spaces>\s*)<!-- OUTPUT:START -->").unwrap());

/// Pattern to match output end with optional leading whitespace.
pub static OUTPUT_END_PATTERN: Lazy<Regex> =
    Lazy::new(|| Regex::new(r"^(?P<spaces>\s*)<!-- OUTPUT:END -->").unwrap());

/// Pattern to match backtick code block start with markdown-code-runner.
pub static CODE_BACKTICKS_START_PATTERN: Lazy<Regex> = Lazy::new(|| {
    Regex::new(r"^(?P<spaces>\s*)```(?P<language>\w+)\s+markdown-code-runner").unwrap()
});

/// Pattern to match backtick code block end.
pub static CODE_BACKTICKS_END_PATTERN: Lazy<Regex> =
    Lazy::new(|| Regex::new(r"^(?P<spaces>\s*)```\s*$").unwrap());

/// Check if a line matches the skip marker.
pub fn is_skip(line: &str) -> bool {
    SKIP_PATTERN.is_match(line)
}

/// Check if a line matches the output start marker.
pub fn is_output_start(line: &str) -> Option<regex::Match<'_>> {
    OUTPUT_START_PATTERN.find(line)
}

/// Check if a line matches the output end marker.
pub fn is_output_end(line: &str) -> bool {
    OUTPUT_END_PATTERN.is_match(line)
}

/// Check if a line matches the Python code comment start marker.
pub fn is_code_comment_python_start(line: &str) -> Option<regex::Match<'_>> {
    CODE_COMMENT_PYTHON_START_PATTERN.find(line)
}

/// Check if a line matches the Bash code comment start marker.
pub fn is_code_comment_bash_start(line: &str) -> Option<regex::Match<'_>> {
    CODE_COMMENT_BASH_START_PATTERN.find(line)
}

/// Check if a line matches the code comment end marker.
pub fn is_code_comment_end(line: &str) -> bool {
    CODE_COMMENT_END_PATTERN.is_match(line)
}

/// Check if a line matches the backticks code block start marker.
pub fn is_code_backticks_start(line: &str) -> Option<regex::Captures<'_>> {
    CODE_BACKTICKS_START_PATTERN.captures(line)
}

/// Check if a line matches the backticks code block end marker.
pub fn is_code_backticks_end(line: &str) -> bool {
    CODE_BACKTICKS_END_PATTERN.is_match(line)
}

/// Remove Markdown comment tags from a string.
/// Returns None if the string is not a valid Markdown comment.
pub fn remove_md_comment(line: &str) -> Option<String> {
    let trimmed = line.trim();
    if trimmed.starts_with("<!-- ") && trimmed.ends_with(" -->") {
        Some(trimmed[5..trimmed.len() - 4].to_string())
    } else {
        None
    }
}

/// Extract leading whitespace from a line.
pub fn get_indent(line: &str) -> String {
    let trimmed = line.trim_start();
    line[..line.len() - trimmed.len()].to_string()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_md_comment() {
        assert_eq!(md_comment("test"), "<!-- test -->");
    }

    #[test]
    fn test_remove_md_comment() {
        assert_eq!(
            remove_md_comment("<!-- This is a comment -->"),
            Some("This is a comment".to_string())
        );
        assert_eq!(remove_md_comment("This is not a comment"), None);
    }

    #[test]
    fn test_is_skip() {
        assert!(is_skip("<!-- CODE:SKIP -->"));
        assert!(is_skip("  <!-- CODE:SKIP -->"));
        assert!(!is_skip("some text"));
    }

    #[test]
    fn test_get_indent() {
        assert_eq!(get_indent("    hello"), "    ");
        assert_eq!(get_indent("hello"), "");
        assert_eq!(get_indent("\t\thello"), "\t\t");
    }

    #[test]
    fn test_is_code_backticks_start() {
        let caps = is_code_backticks_start("```python markdown-code-runner");
        assert!(caps.is_some());
        let caps = caps.unwrap();
        assert_eq!(&caps["language"], "python");

        let caps = is_code_backticks_start("    ```bash markdown-code-runner filename=test.sh");
        assert!(caps.is_some());
        let caps = caps.unwrap();
        assert_eq!(&caps["language"], "bash");
        assert_eq!(&caps["spaces"], "    ");

        assert!(is_code_backticks_start("```python").is_none());
    }
}