mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-29 21:07:24 -06:00
#3692: LaTeX — verbatim delimiter check after control characters go; multicolumn/multirow unwrapping bounded
Finding 57: Writer::code replaced `\end{verbatim}` in a code line and then removed control
characters, so `\end{verb<U+000E>atim}` became a real closing delimiter and the rest of
the line was compiled as TeX. Control characters are now removed first and the
replacement runs on the final text.
Finding 52: the table reader unwrapped every nested \multicolumn / \multirow in a cell,
rescanning the remaining braced content each time (quadratic; the nesting limit didn't
apply). At most MAX_NEST wrappers are unwrapped now; real tables use one or two, which
read as before.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e9fe3d29b6
commit
faa8221bca
@@ -243,8 +243,10 @@ impl Writer {
|
||||
}
|
||||
self.out.push_str("\\begin{verbatim}\n");
|
||||
for l in lines {
|
||||
// The environment ends at the first `\end{verbatim}`, so keep it from appearing.
|
||||
let l: String = l.replace("\\end{verbatim}", "\\end {verbatim}").chars().filter(|c| *c == '\t' || !c.is_control()).collect();
|
||||
// The environment ends at the first `\end{verbatim}`, so keep it from appearing. The
|
||||
// control characters go first: removing one afterwards could join a delimiter back up.
|
||||
let l: String = l.chars().filter(|c| *c == '\t' || !c.is_control()).collect();
|
||||
let l = l.replace("\\end{verbatim}", "\\end {verbatim}");
|
||||
self.out.push_str(&l);
|
||||
self.out.push('\n');
|
||||
}
|
||||
@@ -2671,19 +2673,24 @@ fn table(toks: &[Tok], spec: &str, depth: usize) -> FTable {
|
||||
for cell_toks in r {
|
||||
let mut cell = Cell::default();
|
||||
let mut content = trim_cell(cell_toks);
|
||||
// `\multicolumn{n}{spec}{content}`, `\multirow{n}{width}{content}`.
|
||||
// `\multicolumn{n}{spec}{content}`, `\multirow{n}{width}{content}`. Each wrapper
|
||||
// rescans its content, so only MAX_NEST of them are unwrapped (a cell nests one or
|
||||
// two); further ones stay in the content.
|
||||
let mut sub = Parser::new(content, depth + 1);
|
||||
let mut wrappers = 0usize;
|
||||
loop {
|
||||
sub.skip_spaces();
|
||||
match sub.t.get(sub.pos) {
|
||||
Some(Tok::Cmd(c)) if c == "multicolumn" => {
|
||||
Some(Tok::Cmd(c)) if c == "multicolumn" && wrappers < MAX_NEST => {
|
||||
wrappers += 1;
|
||||
sub.pos += 1;
|
||||
cell.colspan = sub.arg_raw().trim().parse::<u32>().unwrap_or(1).clamp(1, 63);
|
||||
let _ = sub.arg_raw();
|
||||
content = sub.arg_tokens();
|
||||
sub = Parser::new(content, depth + 1);
|
||||
}
|
||||
Some(Tok::Cmd(c)) if c == "multirow" || c == "multirow*" => {
|
||||
Some(Tok::Cmd(c)) if (c == "multirow" || c == "multirow*") && wrappers < MAX_NEST => {
|
||||
wrappers += 1;
|
||||
sub.pos += 1;
|
||||
let _ = sub.opt_arg();
|
||||
cell.rowspan = sub.arg_raw().trim().parse::<i64>().map(|n| n.unsigned_abs().clamp(1, 1000) as u32).unwrap_or(1);
|
||||
@@ -3234,4 +3241,33 @@ Caf\'e na\"ive \c{c}a \v{s}\'{e} and {\bfseries grouped bold} after.
|
||||
let Some(Block::Table(bt_bordered)) = back_bordered.body.first().map(|b| &**b) else { panic!() };
|
||||
assert!(bt_bordered.props.style.is_some(), "imported table should remain bordered");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verbatim_end_is_kept_out_after_control_characters_go() {
|
||||
// A control character inside `\end{verbatim}` was removed after the check for it, so
|
||||
// the code line closed the environment and the rest ran as TeX.
|
||||
let mut d = Document::new();
|
||||
let line = "\\end{verb\u{E}atim}\\input{/etc/passwd}";
|
||||
d.body = vec![wordcraft_doc::para_block(wordcraft_doc::Paragraph::with_text(line, Default::default()).styled(model::CODE_STYLE))];
|
||||
let out = export(&d);
|
||||
assert_eq!(out.matches("\\end{verbatim}").count(), 1, "{out}");
|
||||
assert!(out.contains("\\end {verbatim}\\input"), "{out}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_multicolumn_wrappers_are_bounded() {
|
||||
// Each `\multicolumn` unwrapped rescanned the rest of the cell; nested ones were all
|
||||
// unwrapped, quadratic in the nesting.
|
||||
let n = 50_000;
|
||||
let src = format!("\\begin{{tabular}}{{l}}{}x{}\\\\\\end{{tabular}}", "\\multicolumn{1}{c}{".repeat(n), "}".repeat(n));
|
||||
let t = std::time::Instant::now();
|
||||
let f = parse(&src);
|
||||
assert!(t.elapsed() < std::time::Duration::from_secs(2), "{:?}", t.elapsed());
|
||||
assert!(f.blocks.iter().any(|b| matches!(b, FBlock::Table(_))));
|
||||
// One or two wrappers, as written in real tables, are still read.
|
||||
let f = parse("\\begin{tabular}{ll}\\multicolumn{2}{c}{\\multirow{2}{*}{Both}}\\\\\\end{tabular}");
|
||||
let Some(FBlock::Table(t)) = f.blocks.first() else { panic!("{:?}", f.blocks) };
|
||||
assert_eq!((t.rows[0][0].colspan, t.rows[0][0].rowspan), (2, 2));
|
||||
assert_eq!(t.rows[0][0].text(), "Both");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user