Skip to content

Commit 5002070

Browse files
Merge pull request #95 from developer0hye/perf/optimize-xlsx-conversion
perf(xlsx): stream large sheet rendering
2 parents 79f0b47 + a46dcef commit 5002070

8 files changed

Lines changed: 436 additions & 169 deletions

File tree

Cargo.lock

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

Cargo.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ async-gemini = ["async", "dep:reqwest"]
3131
wasm = ["dep:wasm-bindgen", "dep:js-sys", "dep:wasm-bindgen-futures"]
3232

3333
[dependencies]
34-
zip = { version = "2", default-features = false, features = ["deflate"] }
35-
quick-xml = "0.37"
36-
calamine = { version = "0.26", features = ["dates"] }
34+
zip = { version = "8.2", default-features = false, features = ["deflate"] }
35+
quick-xml = "0.41"
36+
calamine = { version = "0.36", features = ["dates"] }
3737
chrono = { version = "0.4", default-features = false }
3838
csv = "1"
3939
serde_json = "1"

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ A pure Rust tool and library that converts various document formats into Markdow
1818
|--------|-----------|-------|
1919
| DOCX | `.docx` | Headings, tables (incl. merged/layout tables), lists, bold/italic, hyperlinks, images, text boxes |
2020
| PPTX | `.pptx` | Slides, tables (incl. merged cells), speaker notes, images, group shapes |
21-
| XLSX | `.xlsx` | Multi-sheet, date/time handling, images |
22-
| XLS | `.xls` | Legacy Excel (via calamine) |
21+
| XLSX | `.xlsx` | Multi-sheet, date/time handling, images, single-pass table rendering |
22+
| XLS | `.xls` | Legacy Excel (via calamine), single-pass table rendering |
2323
| HTML | `.html`, `.htm` | Full DOM: headings, tables (incl. `colspan`/`rowspan`), lists, links, blockquotes, code blocks |
2424
| CSV | `.csv` | Converted to Markdown tables |
2525
| Jupyter Notebook | `.ipynb` | Markdown cells preserved, code cells in fenced blocks with language detection |
@@ -33,6 +33,8 @@ A pure Rust tool and library that converts various document formats into Markdow
3333

3434
Format is auto-detected from magic bytes and file extension. ZIP-based formats (DOCX/PPTX/XLSX) are distinguished by inspecting internal archive structure.
3535

36+
Large XLSX/XLS sheets render Markdown and plain text together in one row pass. Workbook parsing still uses memory proportional to the worksheet, but conversion does not build a second full cell matrix before producing output.
37+
3638
## Conversion Examples
3739

3840
### CSV

src/converter/docx.rs

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ use zip::ZipArchive;
1515

1616
use crate::converter::comments::{self, Comment};
1717
use crate::converter::ooxml_utils::{
18-
ImageInfo, PendingImageResolution, Relationship, parse_relationships,
19-
resolve_image_placeholders, resolve_relative_to_file,
18+
ImageInfo, PendingImageResolution, Relationship, general_ref_value, parse_relationships,
19+
resolve_image_placeholders, resolve_relative_to_file, text_value_unescaped,
2020
};
2121
use crate::converter::{
2222
ConversionOptions, ConversionResult, ConversionWarning, Converter, WarningCode,
@@ -1150,7 +1150,7 @@ fn parse_document(
11501150
continue;
11511151
}
11521152
if in_text && in_run {
1153-
let text = e.unescape().unwrap_or_default().to_string();
1153+
let text = text_value_unescaped(e);
11541154
let seg = RunSegment {
11551155
text,
11561156
bold: current_run_bold,
@@ -1165,6 +1165,20 @@ fn parse_document(
11651165
}
11661166
}
11671167
}
1168+
Ok(Event::GeneralRef(ref e)) if !in_mc_choice && in_text && in_run => {
1169+
let seg = RunSegment {
1170+
text: general_ref_value(e),
1171+
bold: current_run_bold,
1172+
italic: current_run_italic,
1173+
};
1174+
if in_hyperlink {
1175+
hyperlink_runs.push(seg.clone());
1176+
hyperlink_runs_plain.push(seg);
1177+
} else {
1178+
current_para_runs.push(seg.clone());
1179+
current_para_runs_plain.push(seg);
1180+
}
1181+
}
11681182
Ok(Event::End(ref e)) => {
11691183
let local = e.local_name();
11701184
let local_str = std::str::from_utf8(local.as_ref()).unwrap_or("");
@@ -1737,7 +1751,10 @@ fn parse_comments_xml(xml: &str) -> HashMap<String, RawComment> {
17371751
}
17381752
}
17391753
Ok(Event::Text(ref e)) if in_text => {
1740-
cur_body.push_str(&e.unescape().unwrap_or_default());
1754+
cur_body.push_str(&text_value_unescaped(e));
1755+
}
1756+
Ok(Event::GeneralRef(ref e)) if in_text => {
1757+
cur_body.push_str(&general_ref_value(e));
17411758
}
17421759
Ok(Event::End(ref e)) => {
17431760
let local = e.local_name();
@@ -1933,7 +1950,13 @@ fn collect_ranges_in_part(xml: &str) -> (Vec<String>, HashMap<String, String>) {
19331950
}
19341951
}
19351952
Ok(Event::Text(ref e)) if in_text && in_run && !open.is_empty() => {
1936-
let t = e.unescape().unwrap_or_default();
1953+
let t = text_value_unescaped(e);
1954+
for id in &open {
1955+
push_capped(text.entry(id.clone()).or_default(), &t);
1956+
}
1957+
}
1958+
Ok(Event::GeneralRef(ref e)) if in_text && in_run && !open.is_empty() => {
1959+
let t = general_ref_value(e);
19371960
for id in &open {
19381961
push_capped(text.entry(id.clone()).or_default(), &t);
19391962
}
@@ -2435,6 +2458,18 @@ mod tests {
24352458
assert_eq!(result.markdown.trim(), "Hello, world!");
24362459
}
24372460

2461+
#[test]
2462+
fn test_docx_text_entity_references_preserved() {
2463+
let doc = wrap_body(&para("R&amp;D &lt;ready&gt; &#x1F680;"));
2464+
let data = build_test_docx(&doc, None, None);
2465+
let result = DocxConverter
2466+
.convert(&data, &ConversionOptions::default())
2467+
.unwrap();
2468+
2469+
assert_eq!(result.markdown.trim(), "R&D <ready> 🚀");
2470+
assert_eq!(result.plain_text.trim(), "R&D <ready> 🚀");
2471+
}
2472+
24382473
#[test]
24392474
fn test_docx_multiple_paragraphs() {
24402475
let body = format!("{}{}", para("First paragraph."), para("Second paragraph."));

0 commit comments

Comments
 (0)