Skip to content
Open
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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

53 changes: 43 additions & 10 deletions src/formatter.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use regex::Regex;
use std::fs;
use std::path::Path;
use tokio::fs;
use walkdir::WalkDir;

/// Format a single MDX file with all transformations
Expand Down Expand Up @@ -378,22 +378,35 @@ fn wrap_accordions_in_container(content: &str) -> String {
}

/// Format all MDX files in a directory recursively
pub fn format_all_mdx_files(docs_dir: &Path) -> crate::error::Result<usize> {
let mut modified_count = 0;
pub async fn format_all_mdx_files(docs_dir: &Path) -> crate::error::Result<usize> {
let mut tasks = Vec::new();

for entry in WalkDir::new(docs_dir)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|ext| ext == "mdx"))
{
let path = entry.path();
let original = fs::read_to_string(path)?;
let formatted = format_mdx_file(&original);
let path = entry.path().to_path_buf();

if formatted != original {
fs::write(path, formatted)?;
modified_count += 1;
}
let task = tokio::spawn(async move {
let original = fs::read_to_string(&path).await?;
let formatted = format_mdx_file(&original);

if formatted != original {
fs::write(&path, formatted).await?;
Ok::<usize, std::io::Error>(1)
} else {
Ok::<usize, std::io::Error>(0)
}
});

tasks.push(task);
}

let mut modified_count = 0;
for task in tasks {
let res = task.await.unwrap()?; // unwrap JoinError, then ? for io::Error
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JoinError silently panics instead of propagating

task.await returns Result<Result<usize, std::io::Error>, JoinError>. The .unwrap() on the outer JoinError will panic the entire process if any spawned task panics or is cancelled — instead of returning a recoverable Err through crate::error::Result. Since JoinError doesn't implement From<FumaError>, the automatic ? conversion isn't available, but a manual map is straightforward:

Suggested change
let res = task.await.unwrap()?; // unwrap JoinError, then ? for io::Error
let res = task.await.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))??;

This converts the JoinError into a std::io::Error, which already has a From impl in FumaError::Io, so the second ? then propagates it cleanly through crate::error::Result.

modified_count += res;
}

Ok(modified_count)
Expand Down Expand Up @@ -739,4 +752,24 @@ Final $a$ inline."#;
assert!(output.contains("x = $5"));
assert!(output.contains(r#"let formula = "$$E=mc^2$$";"#));
}

#[tokio::test]
async fn test_format_mdx_file_integration_async() {
let input = r#"<!-- comment -->
# Title
![badge](https://img.shields.io/test)
<br>
<div style="text-align:center;">Content</div>
Math: $x = {1}$
{{% details title="Test" %}}Answer{{% /details %}}"#;

let output = format_mdx_file(input);

// Check all transformations applied
assert!(!output.contains("<!--"));
assert!(!output.contains("shields.io"));
assert!(output.contains("<br />"));
assert!(output.contains("textAlign"));
assert!(output.contains("<Accordion"));
}
Comment on lines +756 to +774
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant async test duplicates existing sync test

test_format_mdx_file_integration_async is byte-for-byte identical in assertions to the already-existing test_format_mdx_file_integration test on line 599. format_mdx_file is a synchronous function — wrapping its call in #[tokio::test] async fn adds no coverage and gives a misleading impression that async behaviour is being exercised. This test can be removed entirely, or replaced with a test that actually exercises the async format_all_mdx_files function (e.g. writing temporary files and asserting on the result).

}
5 changes: 4 additions & 1 deletion src/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,10 @@ pub async fn generate_course_pages(
let frontmatter = build_frontmatter(&title, &course);
let use_course_info = !no_course_info_repo_ids.contains(repo_id);
let page_content = if use_course_info {
format!("{}\n\n<CourseInfo />\n\n{}{}", frontmatter, content, filetree_content)
format!(
"{}\n\n<CourseInfo />\n\n{}{}",
frontmatter, content, filetree_content
)
} else {
format!("{}\n\n{}{}", frontmatter, content, filetree_content)
};
Expand Down
7 changes: 5 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,10 @@ async fn main() -> Result<()> {

let shared_categories_config = loader::load_shared_categories(&data_dir);
if !shared_categories_config.categories.is_empty() {
println!("Loaded {} shared categories", shared_categories_config.categories.len());
println!(
"Loaded {} shared categories",
shared_categories_config.categories.len()
);
}

let grades_summary = loader::load_grades_summary(&data_dir);
Expand Down Expand Up @@ -149,7 +152,7 @@ async fn main() -> Result<()> {

// Format MDX files
println!("Formatting MDX files...");
let modified_count = formatter::format_all_mdx_files(&docs_dir)?;
let modified_count = formatter::format_all_mdx_files(&docs_dir).await?;
println!("Formatted {} MDX files", modified_count);

println!("\n✓ Done! All pages generated and formatted.");
Expand Down
8 changes: 4 additions & 4 deletions src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ mod tests {
title: "Test Course".to_string(),
description: "A test description".to_string(),
course: CourseMetadata {
credit: 3,
credit: 3.0,
assessment_method: "Exam".to_string(),
course_nature: "Required".to_string(),
hour_distribution: HourDistributionMeta {
Expand Down Expand Up @@ -193,7 +193,7 @@ mod tests {
title: "Advanced Math".to_string(),
description: "".to_string(),
course: CourseMetadata {
credit: 4,
credit: 4.0,
assessment_method: "Mixed".to_string(),
course_nature: "Elective".to_string(),
hour_distribution: HourDistributionMeta {
Expand Down Expand Up @@ -238,7 +238,7 @@ mod tests {
title: "Simple Course".to_string(),
description: "No grading details".to_string(),
course: CourseMetadata {
credit: 2,
credit: 2.0,
assessment_method: "Pass/Fail".to_string(),
course_nature: "Optional".to_string(),
hour_distribution: HourDistributionMeta {
Expand All @@ -265,7 +265,7 @@ mod tests {
title: "Complex Course".to_string(),
description: "".to_string(),
course: CourseMetadata {
credit: 5,
credit: 5.0,
assessment_method: "Comprehensive".to_string(),
course_nature: "Core".to_string(),
hour_distribution: HourDistributionMeta {
Expand Down
Loading