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.
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1326,6 +1353,60 @@ mod tests {
|
||||
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 ----
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user