Compare commits

..
7 changed files with 40 additions and 46 deletions
+2 -2
View File
File diff suppressed because one or more lines are too long
+5 -2
View File
@@ -474,14 +474,17 @@ fn parse_blocks(lines: &[String], depth: usize, level: u8, refs: &Refs) -> Vec<F
let p = Para { align: a, inlines: parse_inlines(s, &Fmt::default(), depth, refs), ..Default::default() };
Cell { blocks: vec![FBlock::Para(p)], header, ..Default::default() }
};
table.rows.push(head.iter().zip(&aligns).map(|(s, a)| cell(s, true, *a)).collect());
// A document row keeps MAX_COLS cells: only those are built, so a wide header
// doesn't pad every sparse row below it out to its width.
let cols = aligns.len().min(wordcraft_doc::table::MAX_COLS);
table.rows.push(head.iter().zip(&aligns).take(cols).map(|(s, a)| cell(s, true, *a)).collect());
i += 2;
while let Some(l) = lines.get(i) {
if is_blank(l) || !l.contains('|') || interrupts(l) {
break;
}
let mut cells = split_row(l);
cells.resize(aligns.len(), String::new());
cells.resize(cols, String::new());
table.rows.push(cells.iter().zip(&aligns).map(|(s, a)| cell(s, false, *a)).collect());
i += 1;
}
+14
View File
@@ -714,3 +714,17 @@ fn exported_link_destinations_read_back_unchanged() {
assert_eq!(md("https://example.com/a b"), "[x](<https://example.com/a b>)");
assert_eq!(md("https://e.com/x\ny"), "[x](https://e.com/xy)");
}
#[test]
fn a_wide_table_builds_only_the_cells_a_row_keeps() {
// A 5,000-column header padded every one-cell row below it to 5,000 cells.
let n = 5000;
let md = format!("{}|\n{}|\n{}", "|h".repeat(n), "|-".repeat(n), "|x|\n".repeat(200));
let flow = parse(&md);
let FBlock::Table(t) = &flow.blocks[0] else { panic!("expected table") };
assert_eq!(t.rows.len(), 201);
assert!(t.rows.iter().all(|r| r.len() == wordcraft_doc::table::MAX_COLS));
// A narrow table is unchanged.
let FBlock::Table(t) = &parse("| a | b |\n| - | - |\n| x |\n").blocks[0] else { panic!("expected table") };
assert_eq!(t.rows[1].len(), 2);
}
+5 -23
View File
@@ -323,13 +323,11 @@ fn attr(e: &BytesStart<'_>, name: &[u8]) -> Option<String> {
})
}
/// `s` + `t`, keeping `s` at most [`MAX_CELL_CHARS`] characters. `have` is `s`'s character
/// count, kept up to date here: counting `s` again for every piece of text made a full cell
/// cost its length per further text event.
fn push_capped(s: &mut String, have: &mut usize, t: &str) {
for c in t.chars().take(MAX_CELL_CHARS.saturating_sub(*have)) {
s.push(c);
*have += 1;
/// `s` + `t`, keeping `s` at most [`MAX_CELL_CHARS`] characters.
fn push_capped(s: &mut String, t: &str) {
let have = s.chars().count();
if have < MAX_CELL_CHARS {
s.extend(t.chars().take(MAX_CELL_CHARS - have));
}
}
@@ -446,22 +444,6 @@ mod tests {
])
}
#[test]
fn a_full_cell_takes_further_text_without_recounting() {
let (mut s, mut n) = (String::new(), 0usize);
push_capped(&mut s, &mut n, "é");
push_capped(&mut s, &mut n, &"x".repeat(MAX_CELL_CHARS));
assert_eq!((s.chars().count(), n), (MAX_CELL_CHARS, MAX_CELL_CHARS));
// A million more text events on the full cell each cost nothing (they used to count it).
let t = std::time::Instant::now();
for _ in 0..1_000_000 {
push_capped(&mut s, &mut n, "y");
}
assert!(t.elapsed() < std::time::Duration::from_secs(10), "{:?}", t.elapsed());
assert_eq!(s.chars().count(), MAX_CELL_CHARS);
assert!(!s.contains('y'));
}
/// #1287: one long shared string in thousands of cells stays under the text budget.
#[test]
fn xlsx_shared_strings_cannot_blow_up_memory() {
+6 -8
View File
@@ -11,8 +11,6 @@ struct OCell {
kind: String,
value: Option<String>,
text: String,
/// Characters in `text` (see [`push_capped`]).
text_chars: usize,
paras: usize,
}
@@ -79,7 +77,7 @@ pub(super) fn ods_read(xml: &[u8], want: Option<usize>) -> Result<(Vec<String>,
&& skip == 0
{
if c.paras > 0 {
push_capped(&mut c.text, &mut c.text_chars, "\n");
push_capped(&mut c.text, "\n");
}
c.paras += 1;
}
@@ -88,17 +86,17 @@ pub(super) fn ods_read(xml: &[u8], want: Option<usize>) -> Result<(Vec<String>,
b"s" if in_p > 0 && skip == 0 => {
let k = attr(e, b"c").and_then(|v| v.trim().parse::<usize>().ok()).unwrap_or(1).clamp(1, 1_000);
if let Some(c) = cell.as_mut() {
push_capped(&mut c.text, &mut c.text_chars, &" ".repeat(k));
push_capped(&mut c.text, &" ".repeat(k));
}
}
b"tab" if in_p > 0 && skip == 0 => {
if let Some(c) = cell.as_mut() {
push_capped(&mut c.text, &mut c.text_chars, "\t");
push_capped(&mut c.text, "\t");
}
}
b"line-break" if in_p > 0 && skip == 0 => {
if let Some(c) = cell.as_mut() {
push_capped(&mut c.text, &mut c.text_chars, "\n");
push_capped(&mut c.text, "\n");
}
}
_ => {}
@@ -109,7 +107,7 @@ pub(super) fn ods_read(xml: &[u8], want: Option<usize>) -> Result<(Vec<String>,
&& skip == 0
&& let Some(c) = cell.as_mut()
{
push_capped(&mut c.text, &mut c.text_chars, &t);
push_capped(&mut c.text, &t);
}
}
Ev::End(n) => {
@@ -183,7 +181,7 @@ fn ods_value(c: &OCell) -> String {
("string", Some(v)) if c.text.trim().is_empty() => {
// Attribute text obeys the same per-cell cap as `<text:p>` text.
let mut s = String::new();
push_capped(&mut s, &mut 0, v);
push_capped(&mut s, v);
s
}
_ => c.text.clone(),
+7 -10
View File
@@ -103,18 +103,18 @@ pub(super) fn xlsx_sheets(pkg: &mut Package<'_>) -> Result<Vec<SheetRef>, String
/// (`rPh`) left out.
fn shared_strings(xml: &[u8]) -> Vec<String> {
let mut out = Vec::new();
let mut cur: Option<(String, usize)> = None;
let mut cur: Option<String> = None;
let (mut in_t, mut phonetic) = (false, 0usize);
walk(xml, |ev| match ev {
Ev::Start(e) => match e.local_name().as_ref() {
b"si" => cur = Some((String::new(), 0)),
b"si" => cur = Some(String::new()),
b"t" => in_t = true,
b"rPh" => phonetic += 1,
_ => {}
},
Ev::End(n) => match n.as_slice() {
b"si" => {
if let Some((s, _)) = cur.take()
if let Some(s) = cur.take()
&& out.len() < MAX_SHARED
{
out.push(s);
@@ -127,9 +127,9 @@ fn shared_strings(xml: &[u8]) -> Vec<String> {
Ev::Text(t) => {
if in_t
&& phonetic == 0
&& let Some((s, n)) = cur.as_mut()
&& let Some(s) = cur.as_mut()
{
push_capped(s, n, &t);
push_capped(s, &t);
}
}
});
@@ -183,9 +183,6 @@ struct XCell {
style: usize,
value: String,
inline: String,
/// Characters in `value` and `inline` (see [`push_capped`]).
value_chars: usize,
inline_chars: usize,
}
pub(super) fn xlsx_rows(pkg: &mut Package<'_>, path: &str) -> Result<Vec<Vec<String>>, String> {
@@ -231,9 +228,9 @@ pub(super) fn xlsx_rows(pkg: &mut Package<'_>, path: &str) -> Result<Vec<Vec<Str
Ev::Text(t) => {
if let Some(c) = cell.as_mut() {
if in_v {
push_capped(&mut c.value, &mut c.value_chars, &t);
push_capped(&mut c.value, &t);
} else if in_is_t && phonetic == 0 {
push_capped(&mut c.inline, &mut c.inline_chars, &t);
push_capped(&mut c.inline, &t);
}
}
}
+1 -1
View File
File diff suppressed because one or more lines are too long