66// spell-checker:ignore (ToDO) INFTY MULT accum breakwords linebreak linebreaking linebreaks linelen maxlength minlength nchars ostream overlen parasplit plass posn powf punct signum slen sstart tabwidth tlen underlen winfo wlen wordlen
77
88use std:: io:: { BufWriter , Stdout , Write } ;
9- use std:: { cmp , mem} ;
9+ use std:: mem;
1010
1111use crate :: FmtOptions ;
1212use crate :: parasplit:: { ParaWords , Paragraph , WordInfo } ;
@@ -215,7 +215,7 @@ struct LineBreak<'a> {
215215}
216216
217217#[ allow( clippy:: cognitive_complexity) ]
218- fn find_kp_breakpoints < ' a , T : Iterator < Item = & ' a WordInfo < ' a > > > (
218+ fn find_kp_breakpoints < ' a , T : Clone + Iterator < Item = & ' a WordInfo < ' a > > > (
219219 iter : T ,
220220 args : & BreakArgs < ' a > ,
221221) -> Vec < ( & ' a WordInfo < ' a > , bool ) > {
@@ -226,7 +226,7 @@ fn find_kp_breakpoints<'a, T: Iterator<Item = &'a WordInfo<'a>>>(
226226 linebreak: None ,
227227 break_before: false ,
228228 demerits: 0 ,
229- prev_rat: 0.0 ,
229+ prev_rat: f32 :: NAN ,
230230 length: args. init_len,
231231 fresh: false ,
232232 } ] ;
@@ -243,8 +243,8 @@ fn find_kp_breakpoints<'a, T: Iterator<Item = &'a WordInfo<'a>>>(
243243 } ;
244244 let mut new_linebreaks = vec ! [ ] ;
245245 let mut is_sentence_start = false ;
246- let mut least_demerits = 0 ;
247246 while let Some ( w) = iter. next ( ) {
247+ let next_word_sentence_final = is_next_word_sentence_final ( iter. clone ( ) ) ;
248248 // if this is the last word, we don't add additional demerits for this break
249249 let ( is_last_word, is_sentence_end) = match iter. peek ( ) {
250250 None => ( true , true ) ,
@@ -258,23 +258,51 @@ fn find_kp_breakpoints<'a, T: Iterator<Item = &'a WordInfo<'a>>>(
258258 // should we be adding extra space at the beginning of the next sentence?
259259 let slen = compute_slen ( args. uniform , w. new_line , is_sentence_start, false ) ;
260260
261- let mut ld_new = i64:: MAX ;
262- let mut ld_next = i64:: MAX ;
261+ let mut best_active_demerits = i64:: MAX ;
263262 let mut ld_idx = 0 ;
264263 new_linebreaks. clear ( ) ;
264+ let mut best_break_before: Option < LineBreak < ' _ > > = None ;
265+ let mut best_break_after: Option < LineBreak < ' _ > > = None ;
265266 next_active_breaks. clear ( ) ;
266267 // go through each active break, extending it and possibly adding a new active
267268 // break if we are above the minimum required length
268269 #[ allow( clippy:: explicit_iter_loop) ]
269270 for & i in active_breaks. iter ( ) {
270271 let active = & mut linebreaks[ i] ;
271- // normalize demerits to avoid overflow, and record if this is the least
272- active. demerits -= least_demerits;
273- if active. demerits < ld_next {
274- ld_next = active. demerits ;
272+ if active. demerits < best_active_demerits {
273+ best_active_demerits = active. demerits ;
275274 ld_idx = i;
276275 }
277276
277+ // Also consider a break before this word, so the previous line can end at the
278+ // prior word when that yields a better global layout.
279+ if !active. fresh && active. length >= minlength {
280+ let ( mut new_demerits, new_ratio) = compute_demerits (
281+ args. opts . goal as isize - active. length as isize ,
282+ stretch,
283+ w. word_nchars ,
284+ active. prev_rat ,
285+ ) ;
286+ if is_sentence_end {
287+ new_demerits = new_demerits. saturating_add ( ORPHAN_BREAK_PENALTY ) ;
288+ }
289+ let total_demerits = active. demerits . saturating_add ( new_demerits) ;
290+ if best_break_before
291+ . as_ref ( )
292+ . is_none_or ( |best| total_demerits < best. demerits )
293+ {
294+ best_break_before = Some ( LineBreak {
295+ prev : i,
296+ linebreak : Some ( w) ,
297+ break_before : true ,
298+ demerits : total_demerits,
299+ prev_rat : new_ratio,
300+ length : args. indent_len + w. word_nchars ,
301+ fresh : false ,
302+ } ) ;
303+ }
304+ }
305+
278306 // get the new length
279307 let tlen = w. word_nchars
280308 + args. compute_width ( w, active. length , active. fresh )
@@ -292,7 +320,7 @@ fn find_kp_breakpoints<'a, T: Iterator<Item = &'a WordInfo<'a>>>(
292320
293321 // if we're above the minlength, we can also consider breaking here
294322 if tlen >= minlength {
295- let ( new_demerits, new_ratio) = if is_last_word {
323+ let ( mut new_demerits, new_ratio) = if is_last_word {
296324 // there is no penalty for the final line's length
297325 ( 0 , 0.0 )
298326 } else {
@@ -304,15 +332,16 @@ fn find_kp_breakpoints<'a, T: Iterator<Item = &'a WordInfo<'a>>>(
304332 )
305333 } ;
306334
307- // do not even consider adding a line that has too many demerits
308- // also, try to detect overflow by checking signum
309- let total_demerits = new_demerits + active. demerits ;
310- if new_demerits < BAD_INFTY_SQ
311- && total_demerits < ld_new
312- && active. demerits . signum ( ) <= new_demerits. signum ( )
335+ if !is_last_word && next_word_sentence_final {
336+ new_demerits = new_demerits. saturating_add ( ORPHAN_BREAK_PENALTY ) ;
337+ }
338+
339+ let total_demerits = active. demerits . saturating_add ( new_demerits) ;
340+ if best_break_after
341+ . as_ref ( )
342+ . is_none_or ( |best| total_demerits < best. demerits )
313343 {
314- ld_new = total_demerits;
315- new_linebreaks. push ( LineBreak {
344+ best_break_after = Some ( LineBreak {
316345 prev : i,
317346 linebreak : Some ( w) ,
318347 break_before : false ,
@@ -326,15 +355,16 @@ fn find_kp_breakpoints<'a, T: Iterator<Item = &'a WordInfo<'a>>>(
326355 }
327356 }
328357
329- // if we generated any new linebreaks, add the last one to the list
330- // the last one is always the best because we don't add to new_linebreaks unless
331- // it's better than the best one so far
332- match new_linebreaks. pop ( ) {
333- None => ( ) ,
334- Some ( lb) => {
335- next_active_breaks. push ( linebreaks. len ( ) ) ;
336- linebreaks. push ( lb) ;
337- }
358+ if let Some ( lb) = best_break_before {
359+ new_linebreaks. push ( lb) ;
360+ }
361+ if let Some ( lb) = best_break_after {
362+ new_linebreaks. push ( lb) ;
363+ }
364+
365+ for lb in new_linebreaks. drain ( ..) {
366+ next_active_breaks. push ( linebreaks. len ( ) ) ;
367+ linebreaks. push ( lb) ;
338368 }
339369
340370 if next_active_breaks. is_empty ( ) {
@@ -343,11 +373,6 @@ fn find_kp_breakpoints<'a, T: Iterator<Item = &'a WordInfo<'a>>>(
343373 restart_active_breaks ( args, & linebreaks[ ld_idx] , ld_idx, w, slen, minlength) ;
344374 next_active_breaks. push ( linebreaks. len ( ) ) ;
345375 linebreaks. push ( new_break) ;
346- least_demerits = 0 ;
347- } else {
348- // next time around, normalize out the demerits fields
349- // on active linebreaks to make overflow less likely
350- least_demerits = cmp:: max ( ld_next, 0 ) ;
351376 }
352377 // swap in new list of active breaks
353378 mem:: swap ( & mut active_breaks, & mut next_active_breaks) ;
@@ -382,15 +407,28 @@ fn build_best_path<'a>(paths: &[LineBreak<'a>], active: &[usize]) -> Vec<(&'a Wo
382407 . unwrap_or_default ( )
383408}
384409
385- // "infinite" badness is more like (1+BAD_INFTY)^2 because of how demerits are computed
386- const BAD_INFTY : i64 = 10_000_000 ;
387- const BAD_INFTY_SQ : i64 = BAD_INFTY * BAD_INFTY ;
388410// badness = BAD_MULT * abs(r) ^ 3
389411const BAD_MULT : f32 = 200.0 ;
390412// DR_MULT is multiplier for delta-R between lines
391413const DR_MULT : f32 = 600.0 ;
392414// DL_MULT is penalty multiplier for short words at end of line
393415const DL_MULT : f32 = 10.0 ;
416+ // Penalize breaks that leave the first word on the next line as the sentence-final word.
417+ const ORPHAN_BREAK_PENALTY : i64 = 250_000_000 ;
418+
419+ fn is_word_sentence_final ( current : & WordInfo , next : Option < & WordInfo > ) -> bool {
420+ match next {
421+ None => true ,
422+ Some ( next_word) => next_word. sentence_start || ( next_word. new_line && current. ends_punct ) ,
423+ }
424+ }
425+
426+ fn is_next_word_sentence_final < ' a , T : Iterator < Item = & ' a WordInfo < ' a > > > ( mut iter : T ) -> bool {
427+ let Some ( next_word) = iter. next ( ) else {
428+ return false ;
429+ } ;
430+ is_word_sentence_final ( next_word, iter. next ( ) )
431+ }
394432
395433fn compute_demerits ( delta_len : isize , stretch : usize , wlen : usize , prev_rat : f32 ) -> ( i64 , f32 ) {
396434 // how much stretch are we using?
@@ -401,11 +439,7 @@ fn compute_demerits(delta_len: isize, stretch: usize, wlen: usize, prev_rat: f32
401439 } ;
402440
403441 // compute badness given the stretch ratio
404- let bad_linelen = if ratio. abs ( ) > 1.0f32 {
405- BAD_INFTY
406- } else {
407- ( BAD_MULT * ratio. powi ( 3 ) . abs ( ) ) as i64
408- } ;
442+ let bad_linelen = ( BAD_MULT * ratio. powi ( 3 ) . abs ( ) ) as i64 ;
409443
410444 // we penalize lines ending in really short words
411445 let bad_wordlen = if wlen >= stretch {
@@ -418,9 +452,17 @@ fn compute_demerits(delta_len: isize, stretch: usize, wlen: usize, prev_rat: f32
418452 } ;
419453
420454 // we penalize lines that have very different ratios from previous lines
421- let bad_delta_r = ( DR_MULT * ( ( ratio - prev_rat) / 2.0 ) . powi ( 3 ) . abs ( ) ) as i64 ;
455+ let bad_delta_r = if prev_rat. is_nan ( ) {
456+ 0
457+ } else {
458+ ( DR_MULT * ( ( ratio - prev_rat) / 2.0 ) . powi ( 3 ) . abs ( ) ) as i64
459+ } ;
422460
423- let demerits = i64:: pow ( 1 + bad_linelen + bad_wordlen + bad_delta_r, 2 ) ;
461+ let demerits_base = 1_i64
462+ . saturating_add ( bad_linelen)
463+ . saturating_add ( bad_wordlen)
464+ . saturating_add ( bad_delta_r) ;
465+ let demerits = demerits_base. saturating_mul ( demerits_base) ;
424466
425467 ( demerits, ratio)
426468}
0 commit comments