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.

111 changes: 111 additions & 0 deletions patch_formatter.py
Original file line number Diff line number Diff line change
@@ -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<str> = Cow::Borrowed(content);

// Convert <br> to <br />
let re_br = Regex::new(r"<br\\s*>").unwrap();
if let Cow::Owned(s) = re_br.replace_all(&result, "<br />") {
result = Cow::Owned(s);
}

// Convert <hr> to <hr />
let re_hr = Regex::new(r"<hr\\s*>").unwrap();
if let Cow::Owned(s) = re_hr.replace_all(&result, "<hr />") {
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<str> = Cow::Borrowed(content);

// Remove empty <tr> tags before closing table
let re_tr_table = Regex::new(r"<tr>\\s*</table>").unwrap();
if let Cow::Owned(s) = re_tr_table.replace_all(&result, "</table>") {
result = Cow::Owned(s);
}

// Remove empty <tr></tr> tags
let re_empty_tr = Regex::new(r"<tr>\\s*</tr>").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<str> = 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<str> = 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, "<Accordion title=\\"$1\\">\\n$2\\n</Accordion>") {
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#"<Accordion title="$1">"#) {
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</Accordion>") {
result = Cow::Owned(s);
}

let mut result_owned = result.into_owned();
// Handle any remaining standalone closing tags
result_owned = result_owned.replace("{{% /details %}}", "</Accordion>");

// 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)
66 changes: 41 additions & 25 deletions src/formatter.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use regex::Regex;
use std::borrow::Cow;
use std::fs;
use std::path::Path;
use walkdir::WalkDir;
Expand Down Expand Up @@ -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<str> = Cow::Borrowed(content);

// Convert <br> to <br />
let re_br = Regex::new(r"<br\s*>").unwrap();
result = re_br.replace_all(&result, "<br />").to_string();
if let Cow::Owned(s) = re_br.replace_all(&result, "<br />") {
result = Cow::Owned(s);
}

// Convert <hr> to <hr />
let re_hr = Regex::new(r"<hr\s*>").unwrap();
result = re_hr.replace_all(&result, "<hr />").to_string();
if let Cow::Owned(s) = re_hr.replace_all(&result, "<hr />") {
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<str> = Cow::Borrowed(content);

// Remove empty <tr> tags before closing table
let re_tr_table = Regex::new(r"<tr>\s*</table>").unwrap();
result = re_tr_table.replace_all(&result, "</table>").to_string();
if let Cow::Owned(s) = re_tr_table.replace_all(&result, "</table>") {
result = Cow::Owned(s);
}

// Remove empty <tr></tr> tags
let re_empty_tr = Regex::new(r"<tr>\s*</tr>").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
Expand Down Expand Up @@ -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<str> = 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<str> = 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, "<Accordion title=\"$1\">\n$2\n</Accordion>")
.to_string();
if let Cow::Owned(s) =
re_single_line.replace_all(&result, "<Accordion title=\"$1\">\n$2\n</Accordion>")
{
result = Cow::Owned(s);
}

// Convert opening tags
let re_open = Regex::new(r#"\{\{% details title="([^"]*)"[^%]*%\}\}"#).unwrap();
result = re_open
.replace_all(&result, r#"<Accordion title="$1">"#)
.to_string();
if let Cow::Owned(s) = re_open.replace_all(&result, r#"<Accordion title="$1">"#) {
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</Accordion>")
.to_string();
if let Cow::Owned(s) = re_closing.replace_all(&result, "$1\n</Accordion>") {
result = Cow::Owned(s);
}

let mut result_owned = result.into_owned();
// Handle any remaining standalone closing tags
result = result.replace("{{% /details %}}", "</Accordion>");
result_owned = result_owned.replace("{{% /details %}}", "</Accordion>");

// 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
Expand Down
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
5 changes: 4 additions & 1 deletion 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
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,
Comment on lines 152 to +268
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

PR changes do not match its description

The PR title and description claim this optimizes regex replace operations in src/formatter.rs using std::borrow::Cow, but the actual diff contains no changes to src/formatter.rs at all. Instead, the only substantive change is updating integer credit literals (3, 4, 2, 5) to explicit float literals (3.0, 4.0, 2.0, 5.0) in four test fixtures here in src/models.rs.

While these float literal corrections are technically valid (the credit field is typed as f64, so being explicit is fine), the PR description is entirely incorrect and describes work that was not performed. The Cow-based formatter optimizations mentioned in the description are absent from the changeset.

This mismatch suggests the automated bot may have submitted the wrong branch or the wrong set of changes for this task.

assessment_method: "Comprehensive".to_string(),
course_nature: "Core".to_string(),
hour_distribution: HourDistributionMeta {
Expand Down
Loading