prose: doc-wrap widow control for 1-word orphans
When greedy word-wrap leaves a 1-word last line, pull the penultimate line's tail word down so the orphan has at least 2 words. Skipped if the combined length would exceed budget (invariant takes priority over cosmetics). Visible on nested_let_rec.prose.txt: before: '...captures outer's param\n/// i.' after: '...captures outer's\n/// param i.' Two new unit tests; existing 4 snapshots unaffected (none had a 1-word orphan).
This commit is contained in:
@@ -131,6 +131,33 @@ fn wrap_words(piece: &str, budget: usize) -> Vec<String> {
|
|||||||
// empty line rather than dropping it.
|
// empty line rather than dropping it.
|
||||||
out.push(String::new());
|
out.push(String::new());
|
||||||
}
|
}
|
||||||
|
// Widow control: a 1-word orphan as the last line reads as a
|
||||||
|
// typographic blemish (e.g. "...captures outer's param\ni."). If
|
||||||
|
// the second-to-last line has ≥2 words and the merge fits in
|
||||||
|
// budget, pull its tail word down so the last line has 2 words
|
||||||
|
// and the penultimate line gives up one. Greedy fill never
|
||||||
|
// produces a line that overflows budget when shortened from the
|
||||||
|
// tail, so this never breaks the wrap invariant.
|
||||||
|
if out.len() >= 2 {
|
||||||
|
let last_word_count = out.last().unwrap().split_whitespace().count();
|
||||||
|
if last_word_count == 1 {
|
||||||
|
let prev_idx = out.len() - 2;
|
||||||
|
let prev_words: Vec<&str> = out[prev_idx].split_whitespace().collect();
|
||||||
|
if prev_words.len() >= 2 {
|
||||||
|
let pulled = prev_words.last().unwrap();
|
||||||
|
let last_line = out.last().unwrap();
|
||||||
|
if pulled.len() + 1 + last_line.len() <= budget {
|
||||||
|
let pulled_owned = pulled.to_string();
|
||||||
|
let prev_new = prev_words[..prev_words.len() - 1].join(" ");
|
||||||
|
let last_line_owned = last_line.clone();
|
||||||
|
let combined = format!("{pulled_owned} {last_line_owned}");
|
||||||
|
out[prev_idx] = prev_new;
|
||||||
|
let last_idx = out.len() - 1;
|
||||||
|
out[last_idx] = combined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1326,6 +1353,60 @@ mod tests {
|
|||||||
assert!(doc_lines.len() >= 3, "expected at least 3 doc lines, got:\n{out}");
|
assert!(doc_lines.len() >= 3, "expected at least 3 doc lines, got:\n{out}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn doc_wrap_widow_control_pulls_one_word_back() {
|
||||||
|
// A doc string whose greedy wrap leaves a 1-word orphan as
|
||||||
|
// the last line should pull the previous line's tail word
|
||||||
|
// down so the orphan has at least 2 words. This is exactly
|
||||||
|
// the `nested_let_rec` case where "outer's param i." used
|
||||||
|
// to wrap to "outer's param" / "i.".
|
||||||
|
let doc = "Sum 1 + 2 + ... + n by nested LetRec helpers; inner captures \
|
||||||
|
outer's param i.";
|
||||||
|
let td = TypeDef {
|
||||||
|
name: "Foo".into(),
|
||||||
|
vars: vec![],
|
||||||
|
ctors: vec![Ctor { name: "MkFoo".into(), fields: vec![] }],
|
||||||
|
doc: Some(doc.into()),
|
||||||
|
drop_iterative: false,
|
||||||
|
};
|
||||||
|
let mut out = String::new();
|
||||||
|
write_type_def(&mut out, &td, 0);
|
||||||
|
let doc_lines: Vec<&str> = out.lines().take_while(|l| l.starts_with("///")).collect();
|
||||||
|
// The last doc line MUST have ≥ 2 words.
|
||||||
|
let last = doc_lines.last().unwrap();
|
||||||
|
let last_word_count = last
|
||||||
|
.trim_start_matches("///")
|
||||||
|
.trim()
|
||||||
|
.split_whitespace()
|
||||||
|
.count();
|
||||||
|
assert!(
|
||||||
|
last_word_count >= 2,
|
||||||
|
"expected widow control to leave ≥ 2 words on the last line; got {last:?}"
|
||||||
|
);
|
||||||
|
// No line over the 80-col budget.
|
||||||
|
for line in &doc_lines {
|
||||||
|
assert!(line.len() <= 80, "line over 80 cols: {line:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn doc_wrap_widow_control_skips_when_combined_too_long() {
|
||||||
|
// If the combined length of (pulled + " " + orphan) would
|
||||||
|
// exceed budget, widow control must NOT fire — preserving
|
||||||
|
// the wrap budget invariant takes priority over cosmetics.
|
||||||
|
// Construct a case: prev-tail = a long word, orphan = a long
|
||||||
|
// word — combined > 80.
|
||||||
|
let pieces = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb \
|
||||||
|
cccccccccccccccccccccccccccccccccccccccccc";
|
||||||
|
let lines = wrap_words(pieces, 80);
|
||||||
|
// Sanity: the wrap produced multiple lines.
|
||||||
|
assert!(lines.len() >= 2);
|
||||||
|
// No line exceeds budget.
|
||||||
|
for l in &lines {
|
||||||
|
assert!(l.len() <= 80, "line over budget: {l:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Polish 5: nested match across lines ----
|
// ---- Polish 5: nested match across lines ----
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user