use crate::domain::finding::Finding;
use crate::domain::rule_id::RuleId;
use crate::gates::{GateCtx, GateResult, Violation, read_text};
pub const CITES: &[RuleId] = &[RuleId::TablePipesAreEscaped];
fn unescaped_pipe_in_code(line: &str) -> bool {
let mut code = false;
let mut previous = '\0';
for ch in line.chars() {
if ch == '`' {
code = !code;
}
if code && ch == '|' && previous != '\\' {
return true;
}
previous = ch;
}
false
}
pub fn run(ctx: &GateCtx, files: &[String]) -> GateResult {
let mut violations = Vec::new();
for file in files {
for (number, line) in read_text(ctx, file)?.lines().enumerate() {
if line.starts_with('|') && unescaped_pipe_in_code(line) {
violations.push(Violation::Finding(Finding::on_line(
RuleId::TablePipesAreEscaped,
file,
number + 1,
"",
)));
}
}
}
Ok(violations)
}
#[cfg(test)]
mod tests {
use super::*;
fn run_on(text: &str) -> Vec<String> {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("comparison.md"), text).unwrap();
let ctx = GateCtx::new(dir.path().to_str().unwrap());
run(&ctx, &["comparison.md".to_string()])
.unwrap()
.iter()
.map(ToString::to_string)
.collect()
}
#[test]
fn accepts_an_escaped_pipe_in_a_code_span() {
assert!(run_on("| Pipe | `a\\|b` |\n").is_empty());
}
#[test]
fn rejects_an_unescaped_pipe_in_a_code_span() {
let out = run_on("| Pipe | `a|b` |\n");
assert_eq!(
out,
vec!["FAIL comparison-docs:table-pipes-are-escaped comparison.md:1".to_string()]
);
}
#[test]
fn pipes_between_cells_are_the_table() {
assert!(run_on("| a | b |\n| --- | --- |\n").is_empty());
}
}