Skip to content

Commit 9d9a546

Browse files
committed
div whitespace
1 parent a0921b1 commit 9d9a546

File tree

10 files changed

+427
-14
lines changed

10 files changed

+427
-14
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/qmd-syntax-helper/src/conversions/definition_lists.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ impl DefinitionListConverter {
7878

7979
// Not a definition line, might be next term
8080
if !self.def_item_regex.is_match(potential_term)
81-
|| potential_term.starts_with("::") {
81+
|| potential_term.starts_with("::")
82+
{
8283
// Look ahead for a definition line
8384
let mut j = i + 1;
8485
while j < lines.len() && lines[j].trim().is_empty() {
@@ -87,7 +88,8 @@ impl DefinitionListConverter {
8788

8889
if j < lines.len()
8990
&& self.def_item_regex.is_match(lines[j])
90-
&& !lines[j].starts_with("::") {
91+
&& !lines[j].starts_with("::")
92+
{
9193
// Found another term-definition pair
9294
end_idx = j;
9395
i = j + 1;
@@ -165,12 +167,11 @@ impl DefinitionListConverter {
165167

166168
// Step 2: Use library to convert JSON to markdown
167169
let mut json_reader = std::io::Cursor::new(&pandoc_output.stdout);
168-
let (pandoc_ast, _ctx) = json::read(&mut json_reader)
169-
.context("Failed to parse JSON output from pandoc")?;
170+
let (pandoc_ast, _ctx) =
171+
json::read(&mut json_reader).context("Failed to parse JSON output from pandoc")?;
170172

171173
let mut output = Vec::new();
172-
qmd::write(&pandoc_ast, &mut output)
173-
.context("Failed to write markdown output")?;
174+
qmd::write(&pandoc_ast, &mut output).context("Failed to write markdown output")?;
174175

175176
let result = String::from_utf8(output)
176177
.context("Failed to parse output as UTF-8")?
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
use anyhow::{Context, Result};
2+
use colored::Colorize;
3+
use serde::{Deserialize, Serialize};
4+
use std::fs;
5+
use std::path::Path;
6+
7+
use crate::utils::file_io::{read_file, write_file};
8+
9+
#[derive(Debug, Serialize, Deserialize)]
10+
struct ErrorLocation {
11+
row: usize,
12+
column: usize,
13+
byte_offset: usize,
14+
size: usize,
15+
}
16+
17+
#[derive(Debug, Serialize, Deserialize)]
18+
struct ParseError {
19+
filename: String,
20+
title: String,
21+
message: String,
22+
location: ErrorLocation,
23+
}
24+
25+
pub struct DivWhitespaceConverter {}
26+
27+
impl DivWhitespaceConverter {
28+
pub fn new() -> Result<Self> {
29+
Ok(Self {})
30+
}
31+
32+
/// Parse a file and get error locations as JSON
33+
fn get_parse_errors(&self, file_path: &Path) -> Result<Vec<ParseError>> {
34+
let content = fs::read_to_string(file_path)
35+
.with_context(|| format!("Failed to read file: {}", file_path.display()))?;
36+
37+
// Use the quarto-markdown-pandoc library to parse with JSON error formatter
38+
let mut sink = std::io::sink();
39+
let filename = file_path.to_string_lossy();
40+
41+
let result = quarto_markdown_pandoc::readers::qmd::read(
42+
content.as_bytes(),
43+
false, // not loose mode
44+
&filename,
45+
&mut sink,
46+
Some(
47+
quarto_markdown_pandoc::readers::qmd_error_messages::produce_json_error_messages
48+
as fn(
49+
&[u8],
50+
&quarto_markdown_pandoc::utils::tree_sitter_log_observer::TreeSitterLogObserver,
51+
&str,
52+
) -> Vec<String>,
53+
),
54+
);
55+
56+
match result {
57+
Ok(_) => Ok(Vec::new()), // No errors
58+
Err(error_messages) => {
59+
// Parse the JSON error output
60+
// The error messages come as a single JSON array string
61+
if error_messages.is_empty() {
62+
return Ok(Vec::new());
63+
}
64+
65+
let json_str = error_messages.join("");
66+
let errors: Vec<ParseError> =
67+
serde_json::from_str(&json_str).context("Failed to parse JSON error output")?;
68+
69+
Ok(errors)
70+
}
71+
}
72+
}
73+
74+
/// Find div fence errors that need whitespace fixes
75+
fn find_div_whitespace_errors(&self, content: &str, errors: &[ParseError]) -> Vec<usize> {
76+
let mut fix_positions = Vec::new();
77+
let lines: Vec<&str> = content.lines().collect();
78+
79+
for error in errors {
80+
// Skip errors that are not about div fences
81+
// We're looking for "Missing Space After Div Fence" or errors on lines with :::
82+
let is_div_error = error.title.contains("Div Fence") || error.title == "Parse error";
83+
84+
if !is_div_error {
85+
continue;
86+
}
87+
88+
// The error might be on the line itself or the line before (for div fences)
89+
// Check both the current line and the previous line
90+
let lines_to_check = if error.location.row > 0 {
91+
vec![error.location.row - 1, error.location.row]
92+
} else {
93+
vec![error.location.row]
94+
};
95+
96+
for &line_idx in &lines_to_check {
97+
if line_idx >= lines.len() {
98+
continue;
99+
}
100+
101+
let line = lines[line_idx];
102+
103+
// Check if this line starts with ::: followed immediately by {
104+
let trimmed = line.trim_start();
105+
if let Some(after_colon) = trimmed.strip_prefix(":::") {
106+
if after_colon.starts_with('{') {
107+
// Calculate the position right after :::
108+
// We need byte offset, not char offset
109+
let line_start = content
110+
.lines()
111+
.take(line_idx)
112+
.map(|l| l.len() + 1) // +1 for newline
113+
.sum::<usize>();
114+
115+
let indent_bytes = line.len() - trimmed.len();
116+
let fix_pos = line_start + indent_bytes + 3; // +3 for ":::"
117+
118+
fix_positions.push(fix_pos);
119+
break; // Found it, no need to check other lines for this error
120+
}
121+
}
122+
}
123+
}
124+
125+
// Remove duplicates and sort
126+
fix_positions.sort_unstable();
127+
fix_positions.dedup();
128+
129+
fix_positions
130+
}
131+
132+
/// Apply fixes to content by inserting spaces at specified positions
133+
fn apply_fixes(&self, content: &str, fix_positions: &[usize]) -> String {
134+
let mut result = String::with_capacity(content.len() + fix_positions.len());
135+
let mut last_pos = 0;
136+
137+
for &pos in fix_positions {
138+
// Copy content up to this position
139+
result.push_str(&content[last_pos..pos]);
140+
// Insert a space
141+
result.push(' ');
142+
last_pos = pos;
143+
}
144+
145+
// Copy remaining content
146+
result.push_str(&content[last_pos..]);
147+
148+
result
149+
}
150+
151+
/// Process a single file
152+
pub fn process_file(
153+
&self,
154+
file_path: &Path,
155+
in_place: bool,
156+
check: bool,
157+
verbose: bool,
158+
) -> Result<()> {
159+
let content = read_file(file_path)?;
160+
161+
// Get parse errors
162+
let errors = self.get_parse_errors(file_path)?;
163+
164+
if errors.is_empty() {
165+
if verbose {
166+
println!(" No div whitespace issues found");
167+
}
168+
return Ok(());
169+
}
170+
171+
// Find positions that need fixes
172+
let fix_positions = self.find_div_whitespace_errors(&content, &errors);
173+
174+
if fix_positions.is_empty() {
175+
if verbose {
176+
println!(" No div whitespace issues found");
177+
}
178+
return Ok(());
179+
}
180+
181+
if verbose || check {
182+
println!(
183+
" Found {} div fence(s) needing whitespace fixes",
184+
fix_positions.len().to_string().yellow()
185+
);
186+
}
187+
188+
if check {
189+
println!(" {} No changes written (--check mode)", "✓".green());
190+
return Ok(());
191+
}
192+
193+
// Apply fixes
194+
let new_content = self.apply_fixes(&content, &fix_positions);
195+
196+
if in_place {
197+
write_file(file_path, &new_content)?;
198+
println!(
199+
" {} Fixed {} div fence(s)",
200+
"✓".green(),
201+
fix_positions.len()
202+
);
203+
} else {
204+
// Output to stdout
205+
print!("{}", new_content);
206+
}
207+
208+
Ok(())
209+
}
210+
}

crates/qmd-syntax-helper/src/conversions/grid_tables.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,12 +128,11 @@ impl GridTableConverter {
128128

129129
// Step 2: Use library to convert JSON to markdown
130130
let mut json_reader = std::io::Cursor::new(&pandoc_output.stdout);
131-
let (pandoc_ast, _ctx) = json::read(&mut json_reader)
132-
.context("Failed to parse JSON output from pandoc")?;
131+
let (pandoc_ast, _ctx) =
132+
json::read(&mut json_reader).context("Failed to parse JSON output from pandoc")?;
133133

134134
let mut output = Vec::new();
135-
qmd::write(&pandoc_ast, &mut output)
136-
.context("Failed to write markdown output")?;
135+
qmd::write(&pandoc_ast, &mut output).context("Failed to write markdown output")?;
137136

138137
let result = String::from_utf8(output)
139138
.context("Failed to parse output as UTF-8")?
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
pub mod definition_lists;
2+
pub mod div_whitespace;
23
pub mod grid_tables;

crates/qmd-syntax-helper/src/diagnostics/syntax_check.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,13 @@ impl SyntaxChecker {
7171
false, // not loose mode
7272
&filename,
7373
&mut sink,
74-
None::<fn(&[u8], &quarto_markdown_pandoc::utils::tree_sitter_log_observer::TreeSitterLogObserver, &str) -> Vec<String>>, // no custom error formatter
74+
None::<
75+
fn(
76+
&[u8],
77+
&quarto_markdown_pandoc::utils::tree_sitter_log_observer::TreeSitterLogObserver,
78+
&str,
79+
) -> Vec<String>,
80+
>, // no custom error formatter
7581
);
7682

7783
match result {
@@ -93,7 +99,15 @@ impl SyntaxChecker {
9399
println!("\n{}", "=== Summary ===".bold());
94100
println!("Total files: {}", total);
95101
println!("Successful: {} {}", successes, "✓".green());
96-
println!("Failed: {} {}", failures, if failures > 0 { "✗".red() } else { "✓".green() });
102+
println!(
103+
"Failed: {} {}",
104+
failures,
105+
if failures > 0 {
106+
"✗".red()
107+
} else {
108+
"✓".green()
109+
}
110+
);
97111

98112
if failures > 0 {
99113
let success_rate = (successes as f64 / total as f64) * 100.0;

crates/qmd-syntax-helper/src/main.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ mod diagnostics;
77
mod utils;
88

99
use conversions::definition_lists::DefinitionListConverter;
10+
use conversions::div_whitespace::DivWhitespaceConverter;
1011
use conversions::grid_tables::GridTableConverter;
1112
use diagnostics::syntax_check::SyntaxChecker;
1213
use utils::glob_expand::expand_globs;
@@ -60,6 +61,25 @@ enum Commands {
6061
verbose: bool,
6162
},
6263

64+
/// Fix div fences missing whitespace (:::{ -> ::: {)
65+
FixDivWhitespace {
66+
/// Input files (can be multiple files or glob patterns like "docs/**/*.qmd")
67+
#[arg(required = true)]
68+
files: Vec<String>,
69+
70+
/// Edit files in place
71+
#[arg(short, long)]
72+
in_place: bool,
73+
74+
/// Check mode: show what would be changed without modifying files
75+
#[arg(short, long)]
76+
check: bool,
77+
78+
/// Show verbose output
79+
#[arg(short, long)]
80+
verbose: bool,
81+
},
82+
6383
/// Check syntax of files and report errors
6484
Check {
6585
/// Input files (can be multiple files or glob patterns like "docs/**/*.qmd")
@@ -122,6 +142,25 @@ fn main() -> Result<()> {
122142

123143
Ok(())
124144
}
145+
Commands::FixDivWhitespace {
146+
files,
147+
in_place,
148+
check,
149+
verbose,
150+
} => {
151+
let converter = DivWhitespaceConverter::new()?;
152+
let file_paths = expand_globs(&files)?;
153+
154+
for file_path in file_paths {
155+
if verbose {
156+
println!("Processing: {}", file_path.display());
157+
}
158+
159+
converter.process_file(&file_path, in_place, check, verbose)?;
160+
}
161+
162+
Ok(())
163+
}
125164
Commands::Check {
126165
files,
127166
verbose,

crates/qmd-syntax-helper/src/utils/glob_expand.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ pub fn expand_globs(patterns: &[String]) -> Result<Vec<PathBuf>> {
1616
.with_context(|| format!("Invalid glob pattern: {}", pattern))?;
1717

1818
for path in paths {
19-
let path = path.with_context(|| format!("Failed to read glob match for: {}", pattern))?;
19+
let path =
20+
path.with_context(|| format!("Failed to read glob match for: {}", pattern))?;
2021
files.push(path);
2122
}
2223
} else {

crates/qmd-syntax-helper/src/utils/resources.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use anyhow::{Context, Result};
2-
use include_dir::{include_dir, Dir};
2+
use include_dir::{Dir, include_dir};
33
use std::fs;
44
use std::path::{Path, PathBuf};
55
use std::sync::atomic::{AtomicU64, Ordering};

0 commit comments

Comments
 (0)