Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
entry: mdrefcheck
language: python
types: [markdown]
args: []
require_serial: true
args: []
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,14 @@ pathdiff = "0.2.3"
pulldown-cmark = "0.13.0"
regex = "1.11.2"
walkdir = "2.5.0"

[package.metadata.release]
pre-release-replacements = [
{ file = "pyproject.toml", search = 'version = "[a-z0-9\\.-]+"', replace = 'version = "{{version}}"' },
{ file = "README.md", search = 'rev: v[a-z0-9\\.-]+', replace = 'rev: v{{version}}' },
]
pre-release-commit-message = "chore(release): prepare for {{version}}"

[lints.clippy]
pedantic = "warn"

5 changes: 0 additions & 5 deletions release.toml

This file was deleted.

2 changes: 1 addition & 1 deletion src/checks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ fn check_inline(
validate_section_link(current_path, dest, doc_headings)
}

fn to_exclude(dest: &str, exclude_link_regexes: &Vec<Regex>) -> bool {
fn to_exclude(dest: &str, exclude_link_regexes: &[Regex]) -> bool {
exclude_link_regexes
.iter()
.any(|re| re.is_match(dest.as_ref()))
Expand Down
8 changes: 4 additions & 4 deletions src/checks/email.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
use regex::Regex;

pub fn validate_email(email: &str) -> Result<(), String> {
if !is_valid_email(email) {
Err(format!("Invalid email: {}", email))
} else {
if is_valid_email(email) {
Ok(())
} else {
Err(format!("Invalid email: {email}"))
}
}

/// Email validation according to https://spec.commonmark.org/0.31.2/#email-address
/// Email validation according to <https://spec.commonmark.org/0.31.2/#email-address>
fn is_valid_email(s: &str) -> bool {
static EMAIL_RE: &str = r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$";
Regex::new(EMAIL_RE).unwrap().is_match(s)
Expand Down
6 changes: 3 additions & 3 deletions src/checks/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ pub fn validate_image(current_path: &Path, dest: &str) -> Result<(), String> {
.unwrap_or_else(|| Path::new("."))
.join(dest);

if !resolved.exists() {
Err(format!("Image not found: {}", dest))
} else {
if resolved.exists() {
Ok(())
} else {
Err(format!("Image not found: {dest}"))
}
}
28 changes: 13 additions & 15 deletions src/checks/section.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ pub fn validate_section_link(
) -> Result<(), String> {
let (file_part, heading_part) = dest
.split_once('#')
.map(|(f, h)| (f, Some(h)))
.unwrap_or((dest, None));
.map_or((dest, None), |(f, h)| (f, Some(h)));

let target_file = if file_part.is_empty() {
current_path.to_path_buf()
Expand All @@ -20,24 +19,23 @@ pub fn validate_section_link(
.unwrap_or_else(|| Path::new("."))
.join(file_part);
fs::canonicalize(&resolved)
.map_err(|_| format!("File not found: {}", file_part))?
.map_err(|_| format!("File not found: {file_part}"))?
};

if let Some(heading) = heading_part {
if !section_links
if let Some(heading) = heading_part
&& !section_links
.entry(target_file.clone())
.or_insert_with(|| parser::parse_file_headings(&target_file).unwrap())
.contains(heading)
{
return Err(format!(
"Missing heading #{heading}{}",
if file_part.is_empty() {
"".to_string()
} else {
format!(" in {}", file_part)
}
));
}
{
return Err(format!(
"Missing heading #{heading}{}",
if file_part.is_empty() {
String::new()
} else {
format!(" in {file_part}")
}
));
}

Ok(())
Expand Down
4 changes: 2 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ fn main() {
{
let errors = run_checks(&content, path, &mut section_links, &config);
for err in &errors {
println!("{}", err);
println!("{err}");
}
if !errors.is_empty() {
has_errors = true;
Expand All @@ -35,5 +35,5 @@ fn main() {
process::exit(1);
}

println!("{}", "No broken references found.".green())
println!("{}", "No broken references found.".green());
}
6 changes: 5 additions & 1 deletion src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ use crate::utils::create_options;
pub type SectionLinkMap = HashMap<PathBuf, HashSet<String>>;

/// Scan markdown file and collect section links based on its heading.
/// # Errors
/// This function will return an error if `path` does not already exist.
pub fn parse_file_headings(path: &PathBuf) -> io::Result<HashSet<String>> {
fs::read_to_string(path)
.map(|content| crate::parser::collect_heading_links(&content))
Expand All @@ -34,6 +36,7 @@ pub fn parse_file_headings(path: &PathBuf) -> io::Result<HashSet<String>> {
/// assert!(anchors.contains("intro-1"));
/// assert!(anchors.contains("hello-world"));
/// ```
#[must_use]
pub fn collect_heading_links(content: &str) -> HashSet<String> {
let mut headings = HashSet::new();
let mut heading_counter = HashMap::new();
Expand All @@ -53,7 +56,7 @@ pub fn collect_heading_links(content: &str) -> HashSet<String> {
Event::End(TagEnd::Heading { .. }) => {
let base_link = heading2link(&current_heading);
let link = if let Some(counter) = heading_counter.get_mut(&base_link) {
let numbered_link = format!("{}-{}", base_link, counter);
let numbered_link = format!("{base_link}-{counter}");
*counter += 1;
numbered_link
} else {
Expand Down Expand Up @@ -82,6 +85,7 @@ pub fn collect_heading_links(content: &str) -> HashSet<String> {
/// assert_eq!(heading2link("This -- Is__A_Test!"), "this----is__a_test");
/// assert_eq!(heading2link("A heading with 💡 emoji!"), "a-heading-with--emoji");
/// ```
#[must_use]
pub fn heading2link(text: &str) -> String {
text.to_lowercase()
.chars()
Expand Down
21 changes: 13 additions & 8 deletions src/scanner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@ use walkdir::WalkDir;
use crate::utils::relative_path;

/// Gather markdown files from paths (file or dir)
pub fn gather_markdown_files(
#[must_use]
pub fn gather_markdown_files<S: ::std::hash::BuildHasher>(
paths: &[PathBuf],
exclude: &HashSet<PathBuf>,
exclude: &HashSet<PathBuf, S>,
) -> Vec<PathBuf> {
paths
.iter()
.flat_map(|path| match fs::canonicalize(path) {
Ok(canonical) => collect_markdown_from_path(&canonical, exclude),
Err(_) => {
.flat_map(|path| {
if let Ok(canonical) = fs::canonicalize(path) {
collect_markdown_from_path(&canonical, exclude)
} else {
eprintln!(
"{}",
format!("Skipping invalid path: {}", path.display()).yellow()
Expand All @@ -29,7 +31,10 @@ pub fn gather_markdown_files(
}

/// Collect markdown file(s) from a path (file or dir)
fn collect_markdown_from_path(path: &Path, exclude: &HashSet<PathBuf>) -> Vec<PathBuf> {
fn collect_markdown_from_path<S: ::std::hash::BuildHasher>(
path: &Path,
exclude: &HashSet<PathBuf, S>,
) -> Vec<PathBuf> {
if exclude.contains(path) {
eprintln!(
"{}",
Expand All @@ -50,7 +55,7 @@ fn collect_markdown_from_path(path: &Path, exclude: &HashSet<PathBuf>) -> Vec<Pa
entry
.path()
.canonicalize()
.map_or(false, |p| !exclude.contains(&p))
.is_ok_and(|p| !exclude.contains(&p))
})
.filter_map(Result::ok)
.filter(|entry| is_markdown_file(entry.path()))
Expand All @@ -63,5 +68,5 @@ fn collect_markdown_from_path(path: &Path, exclude: &HashSet<PathBuf>) -> Vec<Pa

/// Determine if the given file path is a markdown file
fn is_markdown_file(path: &Path) -> bool {
path.is_file() && path.extension().map_or(false, |ext| ext == "md")
path.is_file() && path.extension().is_some_and(|ext| ext == "md")
}
9 changes: 7 additions & 2 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,22 @@ use std::{

use pulldown_cmark::Options;

#[must_use]
pub fn create_options() -> Options {
Options::ENABLE_FOOTNOTES | Options::ENABLE_WIKILINKS
}

/// Create HashSet of canonicalized paths from vector of paths
pub fn create_file_set(vec_files: &Vec<PathBuf>) -> HashSet<PathBuf> {
/// Create ``HashSet`` of canonicalized paths from vector of paths
#[must_use]
pub fn create_file_set(vec_files: &[PathBuf]) -> HashSet<PathBuf> {
vec_files
.iter()
.filter_map(|s| fs::canonicalize(s).ok())
.collect()
}

/// Return a path relative to the current working directory
#[must_use]
pub fn relative_path(target: &Path) -> String {
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));

Expand All @@ -33,6 +36,7 @@ pub fn relative_path(target: &Path) -> String {
}

/// Return a Vec where each entry is the byte offset of the start of a line
#[must_use]
pub fn compute_line_starts(text: &str) -> Vec<usize> {
std::iter::once(0)
.chain(
Expand All @@ -43,6 +47,7 @@ pub fn compute_line_starts(text: &str) -> Vec<usize> {
}

/// Convert a byte offset into (line, column) given precomputed line starts
#[must_use]
pub fn offset_to_line_col(offset: usize, line_starts: &[usize]) -> (usize, usize) {
match line_starts.binary_search(&offset) {
Ok(line) => (line + 1, 1), // exact match, first col
Expand Down
Loading