Skip to content

Commit 2bf2318

Browse files
committed
Update README for performance metrics, enhance find command output, and add comprehensive tests for link and reference functionalities
1 parent 769e6e6 commit 2bf2318

File tree

7 files changed

+497
-34
lines changed

7 files changed

+497
-34
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mdref"
3-
version = "0.3.5"
3+
version = "0.3.6"
44
edition = "2024"
55
authors = ["Student Wei <studentweis@gmail.com>"]
66
repository = "https://github.com/studentweis/mdref"

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![crates.io](https://img.shields.io/crates/v/mdref.svg)](https://crates.io/crates/mdref)
44

5-
A fast, Rust-based tool for discovering and migrating Markdown references — it processed 155 directories and 1,561 files in just 1 ms.
5+
A fast, Rust-based tool for discovering and migrating Markdown references — it processed 155 directories and 1,561 files in just 0.1 seconds. Support search by file or directory.
66

77
> [!CAUTION]
88
> This project is still in early development, and some features may not be fully functional. Please use it with caution and report any issues you encounter.
@@ -14,13 +14,13 @@ A fast, Rust-based tool for discovering and migrating Markdown references — it
1414
Install prebuilt binaries via shell script:
1515

1616
```sh
17-
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/studentweis/mdref/releases/download/0.3.5/mdref-installer.sh | sh
17+
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/studentweis/mdref/releases/download/0.3.6/mdref-installer.sh | sh
1818
```
1919

2020
Install prebuilt binaries via powershell script
2121

2222
```sh
23-
powershell -ExecutionPolicy Bypass -c "irm https://github.com/studentweis/mdref/releases/download/0.3.5/mdref-installer.ps1 | iex"
23+
powershell -ExecutionPolicy Bypass -c "irm https://github.com/studentweis/mdref/releases/download/0.3.6/mdref-installer.ps1 | iex"
2424
```
2525

2626
Update mdref:
@@ -58,12 +58,13 @@ Links in ./examples/main.md:
5858

5959
# Todo
6060

61+
- [ ] More tests.
62+
- [ ] Directory path support.
6163
- [ ] Fix the case of link path with space.
64+
- [ ] VSCode extension.
6265
- [ ] Preview mode of mv command.
63-
- [ ] More tests.
6466
- [ ] More documentations.
6567
- [ ] Cargo-dist oranda homepage.
66-
- [ ] VSCode extension.
6768

6869
# Acknowledge
6970

src/commands/find.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ use mdref::{Result, find_links, find_references};
33
pub fn run(file_path: String, root_dir: Option<String>) -> Result<()> {
44
let root_path = root_dir.unwrap_or_else(|| ".".to_string());
55

6+
println!("-------------------------------");
7+
68
// Find references to the specified file.
79
let references = find_references(&file_path, &root_path)?;
810
if references.is_empty() {
@@ -14,6 +16,8 @@ pub fn run(file_path: String, root_dir: Option<String>) -> Result<()> {
1416
}
1517
}
1618

19+
println!("-------------------------------");
20+
1721
// Find all links within the specified file.
1822
let links = find_links(&file_path)?;
1923
if links.is_empty() {
@@ -25,5 +29,7 @@ pub fn run(file_path: String, root_dir: Option<String>) -> Result<()> {
2529
}
2630
}
2731

32+
println!("-------------------------------");
33+
2834
Ok(())
2935
}

src/core/find.rs

Lines changed: 58 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -77,41 +77,76 @@ fn process_md_file(
7777
/// Determine whether a markdown link (found in `file_path`) refers to `target_canonical`.
7878
///
7979
/// - If `target_canonical` is `None`, this function returns `true` (used when simply collecting links).
80-
/// - If `Some(target)`, the link is considered a match only when:
81-
/// 1. The file name component of the link equals the target's file name, and
82-
/// 2. Resolving the link relative to `file_path` and canonicalizing it yields the same absolute path as `target`.
80+
/// - If `Some(target)`, the link is considered a match if:
81+
/// - For files: the file name matches and the resolved path equals the target.
82+
/// - For directories: the resolved path is inside the target directory.
8383
///
84-
/// Returns `true` when both checks succeed; otherwise `false`.
84+
/// Returns `true` when the checks succeed; otherwise `false`.
8585
fn process_link(file_path: &Path, target_canonical: Option<&Path>, link: &str) -> bool {
86-
if let Some(target) = target_canonical {
86+
// If no target specified, accept all links (used for collecting all links)
87+
let target = match target_canonical {
88+
Some(t) => t,
89+
None => return true,
90+
};
91+
92+
// Early check: if target is a file, the link's filename must match
93+
if target.is_file() {
8794
let link_path = Path::new(link);
88-
// Check if filenames match
89-
if link_path.file_name().unwrap() != target.file_name().unwrap() {
95+
if link_path.file_name() != target.file_name() {
9096
return false;
9197
}
92-
// Check if absolute paths match
93-
if let Some(resolved_path) = resolve_link(file_path, link_path) {
94-
matches!(resolved_path.canonicalize(), Ok(canonical) if canonical == *target)
95-
} else {
96-
false
97-
}
98+
}
99+
100+
// Resolve and canonicalize the link path
101+
let canonical_link = match resolve_and_canonicalize_link(file_path, link) {
102+
Some(path) => path,
103+
None => return false,
104+
};
105+
106+
// Match the link against the target
107+
match_link_to_target(&canonical_link, target)
108+
}
109+
110+
/// Resolve a link path and canonicalize it.
111+
///
112+
/// Returns `None` if the link cannot be resolved or canonicalized.
113+
fn resolve_and_canonicalize_link(base_file: &Path, link: &str) -> Option<PathBuf> {
114+
let link_path = Path::new(link);
115+
let resolved = resolve_link(base_file, link_path)?;
116+
resolved.canonicalize().ok()
117+
}
118+
119+
/// Check if a canonicalized link matches the target path.
120+
///
121+
/// For files: the canonical path must match (filename check already done earlier).
122+
/// For directories: the link must resolve to a path inside the target directory.
123+
fn match_link_to_target(canonical_link: &Path, target: &Path) -> bool {
124+
if target.is_file() {
125+
// Filename already checked, just compare canonical paths
126+
canonical_link == target
127+
} else if target.is_dir() {
128+
canonical_link.starts_with(target)
98129
} else {
99-
true
130+
false
100131
}
101132
}
102133

103-
/// Resolve a link relative to the base file path and root directory.
134+
/// Resolve a link relative to the base file path.
135+
///
136+
/// Handles both absolute and relative links.
137+
/// For relative links, resolves them relative to the base file's parent directory.
104138
fn resolve_link(base_path: &Path, link_path: &Path) -> Option<PathBuf> {
105139
if link_path.is_absolute() {
106-
Some(link_path.to_path_buf())
140+
return Some(link_path.to_path_buf());
141+
}
142+
143+
// Resolve relative to the base file's directory
144+
let parent = base_path.parent()?;
145+
let resolved = parent.join(link_path);
146+
147+
if resolved.exists() {
148+
Some(resolved)
107149
} else {
108-
// Try relative to the file's directory first
109-
if let Some(parent) = base_path.parent() {
110-
let resolved = parent.join(link_path);
111-
if resolved.exists() {
112-
return Some(resolved);
113-
}
114-
}
115150
None
116151
}
117152
}

tests/lib_find_tests.rs

Lines changed: 152 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,169 @@
1-
use mdref::{find_links, find_references};
1+
use mdref::{find_links, find_references, Reference};
2+
use std::path::Path;
3+
4+
// ============= find_links tests =============
25

36
#[test]
47
fn test_find_links_nonexistent_file() {
5-
let result = find_links(std::path::Path::new("nonexistent.md"));
8+
let result = find_links(Path::new("nonexistent.md"));
69
assert!(result.is_err());
710
}
811

912
#[test]
1013
fn test_find_links_basic() {
11-
let path = std::path::Path::new("examples/main.md");
14+
let path = Path::new("examples/main.md");
1215
let result = find_links(path).unwrap();
1316
assert!(!result.is_empty());
1417
}
1518

19+
#[test]
20+
fn test_find_links_non_markdown_file() {
21+
// Non-markdown files should return an empty Vec
22+
let path = Path::new("Cargo.toml");
23+
let result = find_links(path).unwrap();
24+
assert!(result.is_empty());
25+
}
26+
27+
#[test]
28+
fn test_find_links_count() {
29+
let path = Path::new("examples/main.md");
30+
let result = find_links(path).unwrap();
31+
// main.md should have 8 links (including image links)
32+
assert_eq!(result.len(), 8);
33+
}
34+
35+
#[test]
36+
fn test_find_links_content_verification() {
37+
let path = Path::new("examples/main.md");
38+
let result = find_links(path).unwrap();
39+
40+
// Verify the found links
41+
let link_texts: Vec<&str> = result.iter().map(|r| r.link_text.as_str()).collect();
42+
assert!(link_texts.contains(&"main.md"));
43+
assert!(link_texts.contains(&"inner/main.md"));
44+
assert!(link_texts.contains(&"other.md"));
45+
}
46+
47+
#[test]
48+
fn test_find_links_line_numbers() {
49+
let path = Path::new("examples/main.md");
50+
let result = find_links(path).unwrap();
51+
52+
// Verify that line numbers are correct (greater than 0)
53+
for reference in result {
54+
assert!(reference.line > 0);
55+
assert!(reference.column > 0);
56+
}
57+
}
58+
59+
// ============= find_references tests =============
60+
1661
#[test]
1762
fn test_find_references_basic() {
18-
let path = std::path::Path::new("examples/main.md");
63+
let path = Path::new("examples/main.md");
1964
let result = find_references(path, path.parent().unwrap()).unwrap();
2065
assert_eq!(result.len(), 6)
2166
}
67+
68+
#[test]
69+
fn test_find_references_nonexistent_file() {
70+
let path = Path::new("nonexistent.md");
71+
let root = Path::new("examples");
72+
let result = find_references(path, root);
73+
assert!(result.is_err());
74+
}
75+
76+
#[test]
77+
fn test_find_references_other_md() {
78+
let path = Path::new("examples/other.md");
79+
let result = find_references(path, path.parent().unwrap()).unwrap();
80+
// other.md is referenced once by main.md
81+
assert!(!result.is_empty());
82+
}
83+
84+
#[test]
85+
fn test_find_references_inner_main() {
86+
let path = Path::new("examples/inner/main.md");
87+
let root = Path::new("examples");
88+
let result = find_references(path, root).unwrap();
89+
// inner/main.md is referenced by the outer main.md and other.md
90+
assert!(result.len() >= 2);
91+
}
92+
93+
#[test]
94+
fn test_find_references_empty_directory() {
95+
let path = Path::new("examples/main.md");
96+
// Use a directory that should have no references
97+
let root = Path::new("benches");
98+
let result = find_references(path, root).unwrap();
99+
// The benches directory should have no references to examples/main.md
100+
assert_eq!(result.len(), 0);
101+
}
102+
103+
#[test]
104+
fn test_find_references_returns_correct_paths() {
105+
let path = Path::new("examples/main.md");
106+
let result = find_references(path, path.parent().unwrap()).unwrap();
107+
108+
// Verify that the returned paths are all markdown files
109+
for reference in result {
110+
assert_eq!(reference.path.extension().and_then(|s| s.to_str()), Some("md"));
111+
}
112+
}
113+
114+
// ============= Reference struct tests =============
115+
116+
#[test]
117+
fn test_reference_creation() {
118+
let reference = Reference::new(
119+
std::path::PathBuf::from("test.md"),
120+
10,
121+
5,
122+
"link.md".to_string()
123+
);
124+
125+
assert_eq!(reference.line, 10);
126+
assert_eq!(reference.column, 5);
127+
assert_eq!(reference.link_text, "link.md");
128+
}
129+
130+
#[test]
131+
fn test_reference_display() {
132+
let reference = Reference::new(
133+
std::path::PathBuf::from("test.md"),
134+
10,
135+
5,
136+
"link.md".to_string()
137+
);
138+
139+
let display_str = format!("{}", reference);
140+
assert!(display_str.contains("test.md"));
141+
assert!(display_str.contains("10"));
142+
assert!(display_str.contains("5"));
143+
assert!(display_str.contains("link.md"));
144+
}
145+
146+
// ============= Edge case tests =============
147+
148+
#[test]
149+
fn test_find_links_empty_markdown_file() {
150+
// Create a temporary empty file for testing
151+
use std::fs;
152+
use std::io::Write;
153+
154+
let temp_file = "test_empty.md";
155+
fs::File::create(temp_file).unwrap().write_all(b"").unwrap();
156+
157+
let result = find_links(Path::new(temp_file)).unwrap();
158+
assert_eq!(result.len(), 0);
159+
160+
// Cleanup
161+
fs::remove_file(temp_file).ok();
162+
}
163+
164+
#[test]
165+
fn test_find_references_with_relative_paths() {
166+
let path = Path::new("examples/main.md");
167+
let result = find_references(path, "examples").unwrap();
168+
assert!(!result.is_empty());
169+
}

0 commit comments

Comments
 (0)