Skip to content

Commit fee206a

Browse files
committed
error messages and parse updates
1 parent 98e4947 commit fee206a

File tree

142 files changed

+116326
-96337
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

142 files changed

+116326
-96337
lines changed
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
use anyhow::{Context, Result};
2+
use std::fs;
3+
use std::path::Path;
4+
5+
use crate::rule::{CheckResult, ConvertResult, Rule, SourceLocation};
6+
use crate::utils::file_io::read_file;
7+
8+
pub struct ApostropheQuotesConverter {}
9+
10+
#[derive(Debug, Clone)]
11+
struct ApostropheViolation {
12+
offset: usize, // Offset of the apostrophe character
13+
error_location: Option<SourceLocation>, // For reporting
14+
}
15+
16+
impl ApostropheQuotesConverter {
17+
pub fn new() -> Result<Self> {
18+
Ok(Self {})
19+
}
20+
21+
/// Get parse errors and extract Q-2-10 apostrophe violations
22+
fn get_apostrophe_violations(&self, file_path: &Path) -> Result<Vec<ApostropheViolation>> {
23+
let content = fs::read_to_string(file_path)
24+
.with_context(|| format!("Failed to read file: {}", file_path.display()))?;
25+
26+
// Parse with quarto-markdown-pandoc to get diagnostics
27+
let mut sink = std::io::sink();
28+
let filename = file_path.to_string_lossy();
29+
30+
let result = quarto_markdown_pandoc::readers::qmd::read(
31+
content.as_bytes(),
32+
false, // not loose mode
33+
&filename,
34+
&mut sink,
35+
true,
36+
);
37+
38+
let diagnostics = match result {
39+
Ok(_) => return Ok(Vec::new()), // No errors
40+
Err(diagnostics) => diagnostics,
41+
};
42+
43+
let mut violations = Vec::new();
44+
45+
for diagnostic in diagnostics {
46+
// Check if this is a Q-2-10 error
47+
if diagnostic.code.as_deref() != Some("Q-2-10") {
48+
continue;
49+
}
50+
51+
// Extract location
52+
let location = diagnostic.location.as_ref();
53+
if location.is_none() {
54+
continue;
55+
}
56+
57+
let offset = location.as_ref().unwrap().start_offset();
58+
59+
violations.push(ApostropheViolation {
60+
offset,
61+
error_location: Some(SourceLocation {
62+
row: self.offset_to_row(&content, offset),
63+
column: self.offset_to_column(&content, offset),
64+
}),
65+
});
66+
}
67+
68+
Ok(violations)
69+
}
70+
71+
/// Apply fixes to the content by inserting backslashes before apostrophes
72+
fn apply_fixes(
73+
&self,
74+
content: &str,
75+
mut violations: Vec<ApostropheViolation>,
76+
) -> Result<String> {
77+
if violations.is_empty() {
78+
return Ok(content.to_string());
79+
}
80+
81+
// Sort violations in reverse order to avoid offset invalidation
82+
violations.sort_by_key(|v| std::cmp::Reverse(v.offset));
83+
84+
let mut result = content.to_string();
85+
86+
for violation in violations {
87+
// The offset points to the space after the apostrophe,
88+
// so we need to insert the backslash at offset-1 (before the apostrophe)
89+
result.insert(violation.offset - 1, '\\');
90+
}
91+
92+
Ok(result)
93+
}
94+
95+
/// Convert byte offset to row number (0-indexed)
96+
fn offset_to_row(&self, content: &str, offset: usize) -> usize {
97+
content[..offset].matches('\n').count()
98+
}
99+
100+
/// Convert byte offset to column number (0-indexed)
101+
fn offset_to_column(&self, content: &str, offset: usize) -> usize {
102+
let line_start = content[..offset]
103+
.rfind('\n')
104+
.map(|pos| pos + 1)
105+
.unwrap_or(0);
106+
offset - line_start
107+
}
108+
}
109+
110+
impl Rule for ApostropheQuotesConverter {
111+
fn name(&self) -> &str {
112+
"apostrophe-quotes"
113+
}
114+
115+
fn description(&self) -> &str {
116+
"Fix Q-2-10: Escape apostrophes misinterpreted as quote closes"
117+
}
118+
119+
fn check(&self, file_path: &Path, _verbose: bool) -> Result<Vec<CheckResult>> {
120+
let violations = self.get_apostrophe_violations(file_path)?;
121+
122+
let results: Vec<CheckResult> = violations
123+
.into_iter()
124+
.map(|v| CheckResult {
125+
rule_name: self.name().to_string(),
126+
file_path: file_path.to_string_lossy().to_string(),
127+
has_issue: true,
128+
issue_count: 1,
129+
message: Some(format!(
130+
"Q-2-10 apostrophe violation at offset {}",
131+
v.offset
132+
)),
133+
location: v.error_location,
134+
error_code: Some("Q-2-10".to_string()),
135+
error_codes: None,
136+
})
137+
.collect();
138+
139+
Ok(results)
140+
}
141+
142+
fn convert(
143+
&self,
144+
file_path: &Path,
145+
in_place: bool,
146+
check_mode: bool,
147+
_verbose: bool,
148+
) -> Result<ConvertResult> {
149+
let content = read_file(file_path)?;
150+
let violations = self.get_apostrophe_violations(file_path)?;
151+
152+
if violations.is_empty() {
153+
return Ok(ConvertResult {
154+
rule_name: self.name().to_string(),
155+
file_path: file_path.to_string_lossy().to_string(),
156+
fixes_applied: 0,
157+
message: Some("No Q-2-10 apostrophe issues found".to_string()),
158+
});
159+
}
160+
161+
let fixed_content = self.apply_fixes(&content, violations.clone())?;
162+
163+
if check_mode {
164+
// Just report what would be done
165+
return Ok(ConvertResult {
166+
rule_name: self.name().to_string(),
167+
file_path: file_path.to_string_lossy().to_string(),
168+
fixes_applied: violations.len(),
169+
message: Some(format!(
170+
"Would fix {} Q-2-10 apostrophe violation(s)",
171+
violations.len()
172+
)),
173+
});
174+
}
175+
176+
if in_place {
177+
// Write back to file
178+
crate::utils::file_io::write_file(file_path, &fixed_content)?;
179+
Ok(ConvertResult {
180+
rule_name: self.name().to_string(),
181+
file_path: file_path.to_string_lossy().to_string(),
182+
fixes_applied: violations.len(),
183+
message: Some(format!(
184+
"Fixed {} Q-2-10 apostrophe violation(s)",
185+
violations.len()
186+
)),
187+
})
188+
} else {
189+
// Return the converted content in message
190+
Ok(ConvertResult {
191+
rule_name: self.name().to_string(),
192+
file_path: file_path.to_string_lossy().to_string(),
193+
fixes_applied: violations.len(),
194+
message: Some(fixed_content),
195+
})
196+
}
197+
}
198+
}

0 commit comments

Comments
 (0)