use super::validator::{IssueKind, ValidationIssue};
#[derive(Debug, Clone)]
pub struct RepairSuggestion {
pub kind: RepairKind,
pub position: usize,
pub tokens: Vec<String>,
pub confidence: f32,
pub description: String,
}
impl RepairSuggestion {
pub fn insert(
position: usize,
tokens: Vec<String>,
confidence: f32,
description: impl Into<String>,
) -> Self {
Self {
kind: RepairKind::Insert,
position,
tokens,
confidence,
description: description.into(),
}
}
pub fn delete(
position: usize,
count: usize,
confidence: f32,
description: impl Into<String>,
) -> Self {
Self {
kind: RepairKind::Delete { count },
position,
tokens: Vec::new(),
confidence,
description: description.into(),
}
}
pub fn replace(
position: usize,
count: usize,
tokens: Vec<String>,
confidence: f32,
description: impl Into<String>,
) -> Self {
Self {
kind: RepairKind::Replace { count },
position,
tokens,
confidence,
description: description.into(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RepairKind {
Insert,
Delete {
count: usize,
},
Replace {
count: usize,
},
}
pub trait RepairStrategy: Send + Sync {
fn suggest(&self, issue: &ValidationIssue, context: &[&str]) -> Vec<RepairSuggestion>;
fn name(&self) -> &str;
}
pub struct BraceRepairStrategy;
impl BraceRepairStrategy {
pub fn new() -> Self {
Self
}
}
impl Default for BraceRepairStrategy {
fn default() -> Self {
Self::new()
}
}
impl RepairStrategy for BraceRepairStrategy {
fn name(&self) -> &str {
"brace-repair"
}
fn suggest(&self, issue: &ValidationIssue, context: &[&str]) -> Vec<RepairSuggestion> {
let mut suggestions = Vec::new();
let pos = issue.position.unwrap_or(0);
match issue.kind {
IssueKind::UnmatchedOpenBrace => {
suggestions.push(RepairSuggestion::insert(
context.len(),
vec!["}".to_string()],
0.7,
"Insert missing closing brace at end",
));
if let Some(break_pos) = find_logical_break(context, pos) {
suggestions.push(RepairSuggestion::insert(
break_pos,
vec!["}".to_string()],
0.8,
format!("Insert closing brace at position {}", break_pos),
));
}
}
IssueKind::UnmatchedCloseBrace => {
suggestions.push(RepairSuggestion::delete(
pos,
1,
0.6,
"Delete unmatched closing brace",
));
suggestions.push(RepairSuggestion::insert(
0,
vec!["{".to_string()],
0.5,
"Insert matching opening brace at start",
));
}
IssueKind::UnmatchedOpenBracket => {
suggestions.push(RepairSuggestion::insert(
context.len(),
vec!["]".to_string()],
0.7,
"Insert missing closing bracket at end",
));
}
IssueKind::UnmatchedCloseBracket => {
suggestions.push(RepairSuggestion::delete(
pos,
1,
0.6,
"Delete unmatched closing bracket",
));
}
IssueKind::UnmatchedOpenParen => {
suggestions.push(RepairSuggestion::insert(
context.len(),
vec![")".to_string()],
0.7,
"Insert missing closing parenthesis at end",
));
}
IssueKind::UnmatchedCloseParen => {
suggestions.push(RepairSuggestion::delete(
pos,
1,
0.6,
"Delete unmatched closing parenthesis",
));
}
_ => {}
}
suggestions
}
}
pub struct EnvironmentRepairStrategy;
impl EnvironmentRepairStrategy {
pub fn new() -> Self {
Self
}
}
impl Default for EnvironmentRepairStrategy {
fn default() -> Self {
Self::new()
}
}
impl RepairStrategy for EnvironmentRepairStrategy {
fn name(&self) -> &str {
"environment-repair"
}
fn suggest(&self, issue: &ValidationIssue, context: &[&str]) -> Vec<RepairSuggestion> {
let mut suggestions = Vec::new();
let pos = issue.position.unwrap_or(0);
match issue.kind {
IssueKind::MissingEnvironmentEnd => {
if let Some(env_name) = extract_begin_env_name(context, pos) {
suggestions.push(RepairSuggestion::insert(
context.len(),
vec![
"\\end".to_string(),
"{".to_string(),
env_name.clone(),
"}".to_string(),
],
0.9,
format!("Insert \\end{{{}}} at end", env_name),
));
}
}
IssueKind::ExtraEnvironmentEnd => {
suggestions.push(RepairSuggestion::delete(
pos,
4, 0.6,
"Delete unmatched \\end",
));
if let Some(env_name) = extract_end_env_name(context, pos) {
suggestions.push(RepairSuggestion::insert(
0,
vec![
"\\begin".to_string(),
"{".to_string(),
env_name.clone(),
"}".to_string(),
],
0.5,
format!("Insert \\begin{{{}}} at start", env_name),
));
}
}
IssueKind::EnvironmentMismatch => {
if let (Some(begin_name), Some(end_pos)) = parse_mismatch_info(&issue.message) {
suggestions.push(RepairSuggestion::replace(
end_pos + 2, 1,
vec![begin_name.clone()],
0.85,
format!("Change \\end to match \\begin{{{}}}", begin_name),
));
}
}
_ => {}
}
suggestions
}
}
pub struct MathRepairStrategy;
impl MathRepairStrategy {
pub fn new() -> Self {
Self
}
}
impl Default for MathRepairStrategy {
fn default() -> Self {
Self::new()
}
}
impl RepairStrategy for MathRepairStrategy {
fn name(&self) -> &str {
"math-repair"
}
fn suggest(&self, issue: &ValidationIssue, context: &[&str]) -> Vec<RepairSuggestion> {
let mut suggestions = Vec::new();
let pos = issue.position.unwrap_or(0);
match issue.kind {
IssueKind::UnmatchedMathDelimiter => {
if pos < context.len() {
let token = context[pos];
match token {
"$" => {
suggestions.push(RepairSuggestion::insert(
context.len(),
vec!["$".to_string()],
0.8,
"Insert closing $",
));
}
"$$" => {
suggestions.push(RepairSuggestion::insert(
context.len(),
vec!["$$".to_string()],
0.8,
"Insert closing $$",
));
}
"\\[" => {
suggestions.push(RepairSuggestion::insert(
context.len(),
vec!["\\]".to_string()],
0.9,
"Insert closing \\]",
));
}
"\\(" => {
suggestions.push(RepairSuggestion::insert(
context.len(),
vec!["\\)".to_string()],
0.9,
"Insert closing \\)",
));
}
"\\]" | "\\)" => {
suggestions.push(RepairSuggestion::delete(
pos,
1,
0.6,
format!("Delete unmatched {}", token),
));
}
_ => {}
}
} else {
suggestions.push(RepairSuggestion::insert(
context.len(),
vec!["$".to_string()],
0.5,
"Insert closing math delimiter",
));
}
}
IssueKind::NestedMathMode => {
suggestions.push(RepairSuggestion::insert(
pos,
vec!["$".to_string()],
0.7,
"Close outer math mode before opening inner",
));
suggestions.push(RepairSuggestion::delete(
pos,
1,
0.6,
"Delete nested math delimiter",
));
}
_ => {}
}
suggestions
}
}
pub struct CompositeRepairStrategy {
strategies: Vec<Box<dyn RepairStrategy>>,
}
impl CompositeRepairStrategy {
pub fn all() -> Self {
Self {
strategies: vec![
Box::new(BraceRepairStrategy::new()),
Box::new(EnvironmentRepairStrategy::new()),
Box::new(MathRepairStrategy::new()),
],
}
}
}
impl RepairStrategy for CompositeRepairStrategy {
fn name(&self) -> &str {
"composite-repair"
}
fn suggest(&self, issue: &ValidationIssue, context: &[&str]) -> Vec<RepairSuggestion> {
let mut all_suggestions = Vec::new();
for strategy in &self.strategies {
all_suggestions.extend(strategy.suggest(issue, context));
}
all_suggestions.sort_by(|a, b| {
b.confidence
.partial_cmp(&a.confidence)
.unwrap_or(std::cmp::Ordering::Equal)
});
all_suggestions
}
}
fn find_logical_break(context: &[&str], start: usize) -> Option<usize> {
for (i, token) in context.iter().enumerate().skip(start) {
if *token == "\n" || *token == "\\end" || *token == ";" || *token == "." {
return Some(i);
}
}
None
}
fn extract_begin_env_name(context: &[&str], pos: usize) -> Option<String> {
if pos + 3 < context.len()
&& context[pos] == "\\begin"
&& context[pos + 1] == "{"
&& context[pos + 3] == "}"
{
Some(context[pos + 2].to_string())
} else {
None
}
}
fn extract_end_env_name(context: &[&str], pos: usize) -> Option<String> {
if pos + 3 < context.len()
&& context[pos] == "\\end"
&& context[pos + 1] == "{"
&& context[pos + 3] == "}"
{
Some(context[pos + 2].to_string())
} else {
None
}
}
fn parse_mismatch_info(message: &str) -> (Option<String>, Option<usize>) {
let begin_start = message.find("\\begin{");
let end_at = message.rfind(" at ");
let begin_name = begin_start.and_then(|start| {
let name_start = start + 7; let name_end = message[name_start..].find('}')?;
Some(message[name_start..name_start + name_end].to_string())
});
let end_pos = end_at.and_then(|at| {
let pos_str = &message[at + 4..];
pos_str.parse::<usize>().ok()
});
(begin_name, end_pos)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_brace_repair_unclosed() {
let strategy = BraceRepairStrategy::new();
let issue = ValidationIssue::error(
IssueKind::UnmatchedOpenBrace,
Some(0),
"Unclosed '{' at position 0",
);
let context = vec!["{", "content"];
let suggestions = strategy.suggest(&issue, &context);
assert!(!suggestions.is_empty());
assert!(suggestions
.iter()
.any(|s| matches!(s.kind, RepairKind::Insert)));
}
#[test]
fn test_brace_repair_extra_close() {
let strategy = BraceRepairStrategy::new();
let issue = ValidationIssue::error(
IssueKind::UnmatchedCloseBrace,
Some(1),
"Unmatched closing brace at position 1",
);
let context = vec!["content", "}"];
let suggestions = strategy.suggest(&issue, &context);
assert!(!suggestions.is_empty());
assert!(suggestions
.iter()
.any(|s| matches!(s.kind, RepairKind::Delete { .. })
|| matches!(s.kind, RepairKind::Insert)));
}
#[test]
fn test_environment_repair_missing_end() {
let strategy = EnvironmentRepairStrategy::new();
let issue = ValidationIssue::error(
IssueKind::MissingEnvironmentEnd,
Some(0),
"Unclosed environment 'equation' starting at position 0",
);
let context = vec!["\\begin", "{", "equation", "}", "x"];
let suggestions = strategy.suggest(&issue, &context);
assert!(!suggestions.is_empty());
assert!(suggestions
.iter()
.any(|s| s.tokens.contains(&"\\end".to_string())));
}
#[test]
fn test_math_repair_unclosed_dollar() {
let strategy = MathRepairStrategy::new();
let issue = ValidationIssue::error(
IssueKind::UnmatchedMathDelimiter,
Some(0),
"Unclosed inline math starting at position 0",
);
let context = vec!["$", "x", "+", "y"];
let suggestions = strategy.suggest(&issue, &context);
assert!(!suggestions.is_empty());
assert!(suggestions
.iter()
.any(|s| s.tokens.contains(&"$".to_string())));
}
#[test]
fn test_composite_strategy() {
let strategy = CompositeRepairStrategy::all();
let issue =
ValidationIssue::error(IssueKind::UnmatchedOpenBrace, Some(0), "Unclosed brace");
let context = vec!["{", "x"];
let suggestions = strategy.suggest(&issue, &context);
assert!(!suggestions.is_empty());
for window in suggestions.windows(2) {
assert!(window[0].confidence >= window[1].confidence);
}
}
#[test]
fn test_repair_suggestion_constructors() {
let insert = RepairSuggestion::insert(5, vec!["}".to_string()], 0.9, "test");
assert!(matches!(insert.kind, RepairKind::Insert));
assert_eq!(insert.position, 5);
let delete = RepairSuggestion::delete(3, 2, 0.7, "test");
assert!(matches!(delete.kind, RepairKind::Delete { count: 2 }));
let replace = RepairSuggestion::replace(1, 1, vec!["new".to_string()], 0.8, "test");
assert!(matches!(replace.kind, RepairKind::Replace { count: 1 }));
}
}