mod blocks;
mod fence;
mod lines;
mod links;
use std::path::Path;
use crate::analysis::findings::{Finding, Severity};
pub const LONG_LINE_MAX: usize = 120;
pub const BLANK_RUN_MAX: usize = 2;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Check {
BareUrl,
EmptyHeading,
LinkSyntaxInvalid,
LongLine,
MissingSpaceAfterHeading,
MultipleBlankLines,
TabCharacter,
TrailingBlankLines,
TrailingWhitespace,
UnclosedCodeFence,
}
impl Check {
pub const ALL: [Check; 10] = [
Check::BareUrl,
Check::EmptyHeading,
Check::LinkSyntaxInvalid,
Check::LongLine,
Check::MissingSpaceAfterHeading,
Check::MultipleBlankLines,
Check::TabCharacter,
Check::TrailingBlankLines,
Check::TrailingWhitespace,
Check::UnclosedCodeFence,
];
pub const fn as_str(self) -> &'static str {
match self {
Check::BareUrl => "bare_url",
Check::EmptyHeading => "empty_heading",
Check::LinkSyntaxInvalid => "link_syntax_invalid",
Check::LongLine => "long_line",
Check::MissingSpaceAfterHeading => "missing_space_after_heading",
Check::MultipleBlankLines => "multiple_blank_lines",
Check::TabCharacter => "tab_character",
Check::TrailingBlankLines => "trailing_blank_lines",
Check::TrailingWhitespace => "trailing_whitespace",
Check::UnclosedCodeFence => "unclosed_code_fence",
}
}
pub const fn severity(self) -> Severity {
match self {
Check::UnclosedCodeFence => Severity::Error,
Check::EmptyHeading | Check::MissingSpaceAfterHeading | Check::LinkSyntaxInvalid => {
Severity::Warning
}
Check::BareUrl
| Check::LongLine
| Check::MultipleBlankLines
| Check::TabCharacter
| Check::TrailingBlankLines
| Check::TrailingWhitespace => Severity::Info,
}
}
pub const fn suggestion(self) -> &'static str {
match self {
Check::BareUrl => "wrap it as [text](url)",
Check::EmptyHeading => "give the heading text, or delete the line",
Check::LinkSyntaxInvalid => "balance the brackets: [text](url)",
Check::LongLine => "wrap or rephrase",
Check::MissingSpaceAfterHeading => "put a space after the `#`s",
Check::MultipleBlankLines => "reduce to one blank line",
Check::TabCharacter => "replace tabs with spaces",
Check::TrailingBlankLines => "remove the blank line(s) at end of file",
Check::TrailingWhitespace => "remove the trailing whitespace",
Check::UnclosedCodeFence => "close it with ```",
}
}
}
pub(crate) struct Line<'a> {
pub number: u32,
pub text: &'a str,
pub in_fence: bool,
}
pub(crate) fn finding(
check: Check,
file_path: &str,
line: u32,
column: u32,
message: String,
) -> Finding {
Finding {
kind: check.as_str().to_owned(),
severity: check.severity(),
file_path: file_path.to_owned(),
line,
column: Some(column),
message,
suggestion: Some(check.suggestion().to_owned()),
}
}
pub fn analyze(path: &Path, content: &str) -> Vec<Finding> {
let file_path = path.to_string_lossy().into_owned();
let raw: Vec<&str> = content.lines().collect();
let fences = fence::Fences::scan(&raw);
let lines: Vec<Line<'_>> = raw
.iter()
.zip(fences.mask())
.enumerate()
.map(|(index, (text, in_fence))| Line {
number: index as u32 + 1,
text,
in_fence: *in_fence,
})
.collect();
let mut findings = Vec::new();
let mut chars: Vec<char> = Vec::new();
let mut scratch: Vec<char> = Vec::new();
for line in &lines {
chars.clear();
chars.extend(line.text.chars());
lines::check(line, &chars, &file_path, &mut findings);
links::check(line, &chars, &mut scratch, &file_path, &mut findings);
}
blocks::check(&lines, &fences, &file_path, &mut findings);
findings.sort_by(|a, b| (a.line, a.column, &a.kind).cmp(&(b.line, b.column, &b.kind)));
findings
}
#[cfg(test)]
mod tests;