use crate::diagnostic::Diagnostic;
use crate::rule::LintRule;
use mdwright_document::Document;
pub struct UnbalancedBacktick;
impl LintRule for UnbalancedBacktick {
fn name(&self) -> &str {
"unbalanced-backtick"
}
fn description(&self) -> &str {
"Backtick in prose that could not be paired with a closing fence."
}
fn explain(&self) -> &str {
include_str!("explain/unbalanced_backtick.md")
}
fn check(&self, doc: &Document, out: &mut Vec<Diagnostic>) {
let source = doc.source().as_bytes();
for chunk in doc.prose_chunks() {
for (idx, _) in chunk.text.match_indices('`') {
if backtick_is_escaped(source, chunk.byte_offset.saturating_add(idx)) {
continue;
}
let message = "unclosed inline code span — pulldown-cmark could not pair \
this backtick with a closing fence"
.to_owned();
if let Some(d) = Diagnostic::at(doc, chunk.byte_offset, idx..idx.saturating_add(1), message, None) {
out.push(d);
}
}
}
}
}
fn backtick_is_escaped(bytes: &[u8], i: usize) -> bool {
let mut count = 0usize;
let mut j = i;
while j > 0 && bytes.get(j.saturating_sub(1)).copied() == Some(b'\\') {
count = count.saturating_add(1);
j = j.saturating_sub(1);
}
count % 2 == 1
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use mdwright_document::Document;
use super::UnbalancedBacktick;
use crate::rule_set::RuleSet;
fn rules() -> Result<RuleSet> {
let mut rs = RuleSet::new();
rs.add(Box::new(UnbalancedBacktick))
.map_err(|e| anyhow::anyhow!("{e}"))?;
Ok(rs)
}
#[test]
fn escaped_backtick_is_not_flagged() -> Result<()> {
let doc = Document::parse(r"Use \`ls\` to list files.")?;
let diags = rules()?.check(&doc);
assert!(diags.is_empty(), "escaped backticks should not flag: {diags:?}");
Ok(())
}
#[test]
fn genuine_unpaired_backtick_is_flagged() -> Result<()> {
let doc = Document::parse("a stray ` backtick here\n")?;
let diags = rules()?.check(&doc);
assert_eq!(diags.len(), 1, "expected one diagnostic: {diags:?}");
Ok(())
}
#[test]
fn even_backslashes_leave_backtick_unescaped() -> Result<()> {
let doc = Document::parse(r"path C:\\` trailing")?;
let diags = rules()?.check(&doc);
assert_eq!(diags.len(), 1, "expected one diagnostic: {diags:?}");
Ok(())
}
}