Skip to content

Commit 27a49f8

Browse files
committed
Refactor metadata injection to eliminate duplication
- Combined inject_teams_into_metadata_table and inject_task_owners_into_metadata_table into single inject_metadata_rows function - Added find_markdown_table_end helper that finds any table by looking for lines starting with '|' - Simplified logic: find table end, insert both rows - no complex conditional checking - Added test for table finding logic
1 parent b443fa7 commit 27a49f8

File tree

1 file changed

+71
-117
lines changed

1 file changed

+71
-117
lines changed

crates/mdbook-goals/src/mdbook_preprocessor.rs

Lines changed: 71 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -481,15 +481,49 @@ impl<'c> GoalPreprocessorWithContext<'c> {
481481
/// that is enforced during goal parsing.
482482
fn replace_metadata_placeholders(&mut self, chapter: &mut Chapter) -> anyhow::Result<()> {
483483
// Auto-inject teams and task owners directly into metadata table instead of using placeholders
484-
self.inject_teams_into_metadata_table(chapter)?;
485-
self.inject_task_owners_into_metadata_table(chapter)?;
484+
self.inject_metadata_rows(chapter)?;
486485

487486
Ok(())
488487
}
489488

490-
/// Automatically inject team names into the metadata table's Teams row.
491-
/// This replaces the need for manual `<!-- TEAMS WITH ASKS -->` placeholders.
492-
fn inject_teams_into_metadata_table(&mut self, chapter: &mut Chapter) -> anyhow::Result<()> {
489+
/// Find the end of a markdown table (first line that doesn't start with |).
490+
/// Returns the byte offset where new rows should be inserted.
491+
fn find_markdown_table_end(content: &str) -> Option<usize> {
492+
let lines: Vec<&str> = content.lines().collect();
493+
494+
// Find first line starting with |
495+
let table_start = lines.iter().position(|line| line.trim_start().starts_with('|'))?;
496+
497+
// Find first line after table_start that doesn't start with |
498+
let table_end = lines[table_start..]
499+
.iter()
500+
.position(|line| !line.trim_start().starts_with('|'))
501+
.map(|pos| table_start + pos)
502+
.unwrap_or(lines.len());
503+
504+
// Calculate byte offset to the start of the table_end line
505+
let mut offset = 0;
506+
for i in 0..table_end {
507+
508+
if i > 0 {
509+
offset += 1; // newline
510+
}
511+
offset += lines[i].len();
512+
}
513+
514+
if table_end < lines.len() {
515+
// There's a line after the table, insert before it
516+
Some(offset + 1) // +1 for the newline after the last table row
517+
} else {
518+
// Table goes to end of file, append at the end
519+
Some(content.len())
520+
}
521+
}
522+
523+
/// Automatically inject team names and task owners into the metadata table.
524+
/// This replaces the need for manual placeholders and combines the logic
525+
/// to avoid duplicate table parsing.
526+
fn inject_metadata_rows(&mut self, chapter: &mut Chapter) -> anyhow::Result<()> {
493527
let Some(chapter_path) = chapter.path.as_ref() else {
494528
return Ok(()); // No path, nothing to inject
495529
};
@@ -500,7 +534,6 @@ impl<'c> GoalPreprocessorWithContext<'c> {
500534
}
501535

502536
// Only process files in milestone directories (like 2024h2, 2025h1, etc.)
503-
// These directories contain actual goal documents
504537
let Some(parent_dir) = chapter_path.parent() else {
505538
return Ok(());
506539
};
@@ -537,77 +570,6 @@ impl<'c> GoalPreprocessorWithContext<'c> {
537570
team_names.join(", ")
538571
};
539572

540-
// First, check if there's already a Teams row and replace it
541-
let teams_row_regex = regex::Regex::new(r"(?m)^\|\s*Teams\s*\|\s*([^|]*)\s*\|").unwrap();
542-
543-
if teams_row_regex.is_match(&chapter.content) {
544-
// Replace existing Teams row
545-
chapter.content = teams_row_regex
546-
.replace(&chapter.content, |_caps: &regex::Captures| {
547-
format!("| Teams | {} |", teams_text)
548-
})
549-
.to_string();
550-
} else {
551-
// No Teams row exists, add one after Point of contact
552-
let metadata_table_regex =
553-
regex::Regex::new(r"(?m)^(\| Point of contact \| [^|]+ \|)").unwrap();
554-
555-
if metadata_table_regex.is_match(&chapter.content) {
556-
chapter.content = metadata_table_regex
557-
.replace(&chapter.content, |caps: &regex::Captures| {
558-
format!(
559-
"{}\n| Teams | {} |",
560-
caps.get(1).unwrap().as_str(),
561-
teams_text
562-
)
563-
})
564-
.to_string();
565-
}
566-
}
567-
568-
Ok(())
569-
}
570-
571-
/// Automatically inject task owner names into the metadata table's Task owners row.
572-
/// This replaces the need for manual `<!-- TASK OWNERS -->` placeholders.
573-
fn inject_task_owners_into_metadata_table(
574-
&mut self,
575-
chapter: &mut Chapter,
576-
) -> anyhow::Result<()> {
577-
let Some(chapter_path) = chapter.path.as_ref() else {
578-
return Ok(()); // No path, nothing to inject
579-
};
580-
581-
// Skip template files
582-
if chapter_path.file_name().and_then(|n| n.to_str()) == Some("TEMPLATE.md") {
583-
return Ok(());
584-
}
585-
586-
// Only process files in milestone directories (like 2024h2, 2025h1, etc.)
587-
// These directories contain actual goal documents
588-
let Some(parent_dir) = chapter_path.parent() else {
589-
return Ok(());
590-
};
591-
592-
let Some(parent_name) = parent_dir.file_name().and_then(|n| n.to_str()) else {
593-
return Ok(());
594-
};
595-
596-
if !parent_name
597-
.chars()
598-
.next()
599-
.map_or(false, |c| c.is_ascii_digit())
600-
{
601-
return Ok(()); // Not a milestone directory, skip
602-
}
603-
604-
// Find the goal document for this chapter
605-
let goals = self.goal_documents(&chapter_path)?;
606-
let chapter_in_context = self.ctx.config.book.src.join(chapter_path);
607-
let Some(goal) = goals.iter().find(|gd| gd.path == chapter_in_context) else {
608-
return Ok(()); // No goal document found, nothing to inject
609-
};
610-
611573
// Compute the task owner names
612574
let task_owners: Vec<String> = goal.task_owners.iter().cloned().collect();
613575

@@ -617,48 +579,40 @@ impl<'c> GoalPreprocessorWithContext<'c> {
617579
task_owners.join(", ")
618580
};
619581

620-
// First, check if there's already a Task owners row and replace it
621-
let task_owners_row_regex =
622-
regex::Regex::new(r"(?m)^\|\s*Task owners\s*\|\s*([^|]*)\s*\|").unwrap();
623-
624-
if task_owners_row_regex.is_match(&chapter.content) {
625-
// Replace existing Task owners row
626-
chapter.content = task_owners_row_regex
627-
.replace(&chapter.content, |_caps: &regex::Captures| {
628-
format!("| Task owners | {} |", task_owners_text)
629-
})
630-
.to_string();
631-
} else {
632-
// No Task owners row exists, add one after Teams row (or Point of contact if no Teams row)
633-
let after_teams_regex = regex::Regex::new(r"(?m)^(\| Teams\s*\| [^|]+ \|)").unwrap();
634-
let after_contact_regex =
635-
regex::Regex::new(r"(?m)^(\| Point of contact \| [^|]+ \|)").unwrap();
636-
637-
if after_teams_regex.is_match(&chapter.content) {
638-
// Add after Teams row
639-
chapter.content = after_teams_regex
640-
.replace(&chapter.content, |caps: &regex::Captures| {
641-
format!(
642-
"{}\n| Task owners | {} |",
643-
caps.get(1).unwrap().as_str(),
644-
task_owners_text
645-
)
646-
})
647-
.to_string();
648-
} else if after_contact_regex.is_match(&chapter.content) {
649-
// Add after Point of contact row
650-
chapter.content = after_contact_regex
651-
.replace(&chapter.content, |caps: &regex::Captures| {
652-
format!(
653-
"{}\n| Task owners | {} |",
654-
caps.get(1).unwrap().as_str(),
655-
task_owners_text
656-
)
657-
})
658-
.to_string();
659-
}
582+
// Find the table end and insert both rows
583+
if let Some(table_end) = Self::find_markdown_table_end(&chapter.content) {
584+
let insertion_text = format!(
585+
"| Teams | {} |\n| Task owners | {} |\n",
586+
teams_text, task_owners_text
587+
);
588+
chapter.content.insert_str(table_end, &insertion_text);
660589
}
661590

662591
Ok(())
663592
}
664593
}
594+
595+
#[cfg(test)]
596+
mod tests {
597+
use super::*;
598+
599+
#[test]
600+
fn test_find_markdown_table_end() {
601+
let content = "Some text before\n\n| Metadata | Value |\n|----------|-------|\n| Point of contact | @nikomatsakis |\n| Teams | (none) |\n\nSome text after";
602+
603+
let result = GoalPreprocessorWithContext::find_markdown_table_end(content);
604+
assert!(result.is_some());
605+
606+
let offset = result.unwrap();
607+
let (before, after) = content.split_at(offset);
608+
609+
// Should split right before the blank line after the table
610+
assert!(before.ends_with("| Teams | (none) |\n"));
611+
assert!(after.starts_with("\nSome text after"));
612+
613+
// Test that inserting at this offset works correctly
614+
let mut test_content = content.to_string();
615+
test_content.insert_str(offset, "| New row | value |\n");
616+
assert!(test_content.contains("| Teams | (none) |\n| New row | value |\n\nSome text after"));
617+
}
618+
}

0 commit comments

Comments
 (0)