diff --git a/Cargo.lock b/Cargo.lock index 4d200ec..64bb015 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -318,7 +318,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "hoa-backend" -version = "1.15.0" +version = "1.15.1" dependencies = [ "base64", "chrono", diff --git a/patch_formatter.py b/patch_formatter.py new file mode 100644 index 0000000..82b74f6 --- /dev/null +++ b/patch_formatter.py @@ -0,0 +1,111 @@ +import re + +with open('src/formatter.rs', 'r') as f: + content = f.read() + +def replace_func(func_name, code): + # Find the function in the file + pattern = rf"(fn {func_name}\(content: &str\) -> String {{\n)(.*?)(^\}})" + match = re.search(pattern, content, re.DOTALL | re.MULTILINE) + if not match: + print(f"Could not find {func_name}") + return content + return content[:match.start()] + f"fn {func_name}(content: &str) -> String {{\n" + code + "}" + content[match.end():] + +# fix_self_closing_tags +fix_self_closing_tags_code = """ let mut result: Cow = Cow::Borrowed(content); + + // Convert
to
+ let re_br = Regex::new(r"").unwrap(); + if let Cow::Owned(s) = re_br.replace_all(&result, "
") { + result = Cow::Owned(s); + } + + // Convert
to
+ let re_hr = Regex::new(r"").unwrap(); + if let Cow::Owned(s) = re_hr.replace_all(&result, "
") { + result = Cow::Owned(s); + } + + result.into_owned() +""" +content = replace_func("fix_self_closing_tags", fix_self_closing_tags_code) + +# fix_malformed_html +fix_malformed_html_code = """ let mut result: Cow = Cow::Borrowed(content); + + // Remove empty tags before closing table + let re_tr_table = Regex::new(r"\\s*").unwrap(); + if let Cow::Owned(s) = re_tr_table.replace_all(&result, "") { + result = Cow::Owned(s); + } + + // Remove empty tags + let re_empty_tr = Regex::new(r"\\s*").unwrap(); + if let Cow::Owned(s) = re_empty_tr.replace_all(&result, "") { + result = Cow::Owned(s); + } + + result.into_owned() +""" +content = replace_func("fix_malformed_html", fix_malformed_html_code) + +# convert_hugo_callout_shortcodes +convert_hugo_callout_shortcodes_code = """ let mut result: Cow = Cow::Borrowed(content); + + // Remove opening callout tags such as: + // {{< callout type="info" >}} or {{% callout type="warning" %}} + let re_open = Regex::new(r"\\{\\{[<%]\\s*callout\\b[^{}]*[>%]\\}\\}").unwrap(); + if let Cow::Owned(s) = re_open.replace_all(&result, "") { + result = Cow::Owned(s); + } + + // Remove closing callout tags such as: + // {{< /callout >}} or {{% /callout %}} + let re_close = Regex::new(r"\\{\\{[<%]\\s*/callout\\s*[>%]\\}\\}").unwrap(); + if let Cow::Owned(s) = re_close.replace_all(&result, "") { + result = Cow::Owned(s); + } + + result.into_owned() +""" +content = replace_func("convert_hugo_callout_shortcodes", convert_hugo_callout_shortcodes_code) + +# convert_hugo_details_to_accordion +convert_hugo_details_to_accordion_code = """ let mut result: Cow = Cow::Borrowed(content); + + // First, handle single-line shortcodes: {{% details title="..." %}} content {{% /details %}} + let re_single_line = + Regex::new(r#"\\{\\{% details title="([^"]*)"[^%]*%\\}\\}\\s*(.+?)\\s*\\{\\{% /details %\\}\\}"#) + .unwrap(); + if let Cow::Owned(s) = re_single_line.replace_all(&result, "\\n$2\\n") { + result = Cow::Owned(s); + } + + // Convert opening tags + let re_open = Regex::new(r#"\\{\\{% details title="([^"]*)"[^%]*%\\}\\}"#).unwrap(); + if let Cow::Owned(s) = re_open.replace_all(&result, r#""#) { + result = Cow::Owned(s); + } + + // Convert closing tags - ensure they're on their own line for MDX compatibility + // Replace any occurrence where {{% /details %}} appears at end of line content + let re_closing = Regex::new(r#"([^\\n])\\s*\\{\\{% /details %\\}\\}"#).unwrap(); + if let Cow::Owned(s) = re_closing.replace_all(&result, "$1\\n") { + result = Cow::Owned(s); + } + + let mut result_owned = result.into_owned(); + // Handle any remaining standalone closing tags + result_owned = result_owned.replace("{{% /details %}}", ""); + + // Wrap consecutive Accordion blocks in Accordions + result_owned = wrap_accordions_in_container(&result_owned); + + result_owned +""" +content = replace_func("convert_hugo_details_to_accordion", convert_hugo_details_to_accordion_code) + + +with open('src/formatter.rs', 'w') as f: + f.write(content) diff --git a/src/formatter.rs b/src/formatter.rs index 634dd21..a79871d 100644 --- a/src/formatter.rs +++ b/src/formatter.rs @@ -1,4 +1,5 @@ use regex::Regex; +use std::borrow::Cow; use std::fs; use std::path::Path; use walkdir::WalkDir; @@ -49,32 +50,40 @@ fn remove_shield_badges(content: &str) -> String { /// Convert HTML tags to self-closing format for MDX compatibility fn fix_self_closing_tags(content: &str) -> String { - let mut result = content.to_string(); + let mut result: Cow = Cow::Borrowed(content); // Convert
to
let re_br = Regex::new(r"").unwrap(); - result = re_br.replace_all(&result, "
").to_string(); + if let Cow::Owned(s) = re_br.replace_all(&result, "
") { + result = Cow::Owned(s); + } // Convert
to
let re_hr = Regex::new(r"").unwrap(); - result = re_hr.replace_all(&result, "
").to_string(); + if let Cow::Owned(s) = re_hr.replace_all(&result, "
") { + result = Cow::Owned(s); + } - result + result.into_owned() } /// Fix common malformed HTML patterns fn fix_malformed_html(content: &str) -> String { - let mut result = content.to_string(); + let mut result: Cow = Cow::Borrowed(content); // Remove empty tags before closing table let re_tr_table = Regex::new(r"\s*").unwrap(); - result = re_tr_table.replace_all(&result, "").to_string(); + if let Cow::Owned(s) = re_tr_table.replace_all(&result, "") { + result = Cow::Owned(s); + } // Remove empty tags let re_empty_tr = Regex::new(r"\s*").unwrap(); - result = re_empty_tr.replace_all(&result, "").to_string(); + if let Cow::Owned(s) = re_empty_tr.replace_all(&result, "") { + result = Cow::Owned(s); + } - result + result.into_owned() } /// Convert CSS property name to camelCase for JSX @@ -130,53 +139,60 @@ fn convert_style_to_jsx(content: &str) -> String { /// Remove Hugo callout shortcodes that are invalid in MDX. fn convert_hugo_callout_shortcodes(content: &str) -> String { - let mut result = content.to_string(); + let mut result: Cow = Cow::Borrowed(content); // Remove opening callout tags such as: // {{< callout type="info" >}} or {{% callout type="warning" %}} let re_open = Regex::new(r"\{\{[<%]\s*callout\b[^{}]*[>%]\}\}").unwrap(); - result = re_open.replace_all(&result, "").to_string(); + if let Cow::Owned(s) = re_open.replace_all(&result, "") { + result = Cow::Owned(s); + } // Remove closing callout tags such as: // {{< /callout >}} or {{% /callout %}} let re_close = Regex::new(r"\{\{[<%]\s*/callout\s*[>%]\}\}").unwrap(); - result = re_close.replace_all(&result, "").to_string(); + if let Cow::Owned(s) = re_close.replace_all(&result, "") { + result = Cow::Owned(s); + } - result + result.into_owned() } /// Convert Hugo details shortcode to Fumadocs Accordion components fn convert_hugo_details_to_accordion(content: &str) -> String { - let mut result = content.to_string(); + let mut result: Cow = Cow::Borrowed(content); // First, handle single-line shortcodes: {{% details title="..." %}} content {{% /details %}} let re_single_line = Regex::new(r#"\{\{% details title="([^"]*)"[^%]*%\}\}\s*(.+?)\s*\{\{% /details %\}\}"#) .unwrap(); - result = re_single_line - .replace_all(&result, "\n$2\n") - .to_string(); + if let Cow::Owned(s) = + re_single_line.replace_all(&result, "\n$2\n") + { + result = Cow::Owned(s); + } // Convert opening tags let re_open = Regex::new(r#"\{\{% details title="([^"]*)"[^%]*%\}\}"#).unwrap(); - result = re_open - .replace_all(&result, r#""#) - .to_string(); + if let Cow::Owned(s) = re_open.replace_all(&result, r#""#) { + result = Cow::Owned(s); + } // Convert closing tags - ensure they're on their own line for MDX compatibility // Replace any occurrence where {{% /details %}} appears at end of line content let re_closing = Regex::new(r#"([^\n])\s*\{\{% /details %\}\}"#).unwrap(); - result = re_closing - .replace_all(&result, "$1\n") - .to_string(); + if let Cow::Owned(s) = re_closing.replace_all(&result, "$1\n") { + result = Cow::Owned(s); + } + let mut result_owned = result.into_owned(); // Handle any remaining standalone closing tags - result = result.replace("{{% /details %}}", ""); + result_owned = result_owned.replace("{{% /details %}}", ""); // Wrap consecutive Accordion blocks in Accordions - result = wrap_accordions_in_container(&result); + result_owned = wrap_accordions_in_container(&result_owned); - result + result_owned } /// Convert block-level math delimiters $$ $$ to ```math code blocks diff --git a/src/generator.rs b/src/generator.rs index c907960..2f51ca8 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -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\n\n{}{}", frontmatter, content, filetree_content) + format!( + "{}\n\n\n\n{}{}", + frontmatter, content, filetree_content + ) } else { format!("{}\n\n{}{}", frontmatter, content, filetree_content) }; diff --git a/src/main.rs b/src/main.rs index 527ec54..1728f0d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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); diff --git a/src/models.rs b/src/models.rs index 63d9e55..5a4063a 100644 --- a/src/models.rs +++ b/src/models.rs @@ -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 { @@ -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 { @@ -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 { @@ -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 {