mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-03 01:08:04 +00:00
Merge PR #2739: Layout: a line can break after a hyphen before a digit, like the reference app (#2712)
crates/layout/src/tests.rs: both sides' new tests kept.
This commit is contained in:
commit
b7776ee6ac
4 files changed
+90
-1
No files matched your search
@@ -523,6 +523,7 @@ Footer page-table height and body wrapping are corrected. Other pagination gaps
|
||||
| 2026-10-19 | trivial | Progress log: forms protection keeps its content controls: no inserting, Properties or Remove Content Control |
|
||||
| 2026-10-19 | trivial | Progress log: text typed into a content control takes the control's formatting; localized placeholder styles recognised |
|
||||
| 2026-10-17 | trivial | Progress log: #2048 entry covers anchors pushed onto the next page |
|
||||
| 2026-10-17 | trivial | A line breaks after a hyphen before a digit, like Word (#2712) |
|
||||
| 2026-10-15 | trivial | Progress log: text above a float's anchor wraps around it, #2048 |
|
||||
| 2026-10-18 | trivial | Progress log: long navigation headings wrap within the pane instead of ending in an ellipsis (#3015) |
|
||||
| 2026-10-18 | trivial | Font list starts at its first row after clearing a no-match query; recent heading remains visible (#2068) |
|
||||
|
||||
@@ -1775,6 +1775,21 @@ pub fn layout_para(p: &Paragraph, env: &ParaEnv) -> ParaLayout {
|
||||
.collect();
|
||||
if !before_slash.is_empty() {
|
||||
found.extend(before_slash);
|
||||
}
|
||||
|
||||
// Word breaks after a hyphen even when a digit follows ("2016-" / "2022"), where UAX #14
|
||||
// keeps a hyphen and a number together (LB25): add those positions back. Only U+002D before
|
||||
// ASCII digits is added here; a non-breaking hyphen (U+2011) and a minus sign (U+2212) stay
|
||||
// unbreakable, and the other dashes keep their ordinary UAX #14 breaks.
|
||||
let hyphen_breaks: Vec<usize> = text
|
||||
.match_indices('-')
|
||||
.filter_map(|(pos, _)| {
|
||||
let i = pos + 1;
|
||||
text.get(i..).and_then(|t| t.chars().next()).is_some_and(|n| n.is_ascii_digit()).then_some(i)
|
||||
})
|
||||
.collect();
|
||||
if !hyphen_breaks.is_empty() {
|
||||
found.extend(hyphen_breaks);
|
||||
found.sort_unstable();
|
||||
found.dedup();
|
||||
}
|
||||
|
||||
@@ -9787,3 +9787,75 @@ fn keep_next_counts_list_indent() {
|
||||
.collect();
|
||||
assert_eq!(labels, ["1."], "lookahead must not consume a list counter");
|
||||
}
|
||||
|
||||
/// #2712: Word breaks a line right after a hyphen followed by a digit ("2016-" / "2022"), where
|
||||
/// UAX #14 keeps the hyphen and the number together. Sweeping the text width, the first width
|
||||
/// that wraps `Deed 2016-2022` must break after the hyphen -- not move the range whole (the
|
||||
/// break lands at the space) and not split it inside a number.
|
||||
#[test]
|
||||
fn a_hyphen_before_digits_is_a_break_opportunity_like_word() {
|
||||
for m in 100..=540 {
|
||||
let margin_right = m as f32;
|
||||
let text = "Deed 2016-2022";
|
||||
let mut d = Document::from_text(text);
|
||||
d.last_section.margin_right = margin_right;
|
||||
let l = lay(&d);
|
||||
let line_texts = |l: &DocLayout| -> Vec<String> {
|
||||
l.pages[0]
|
||||
.items
|
||||
.iter()
|
||||
.filter_map(|it| {
|
||||
if let Placed::Lines { para, l0, l1, .. } = it {
|
||||
Some(para.lines[*l0..*l1].iter().map(|line| text[line.start..line.stop].to_string()).collect::<Vec<_>>())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
.collect()
|
||||
};
|
||||
let lines = line_texts(&l);
|
||||
if lines.len() < 2 {
|
||||
continue; // still fits on one line at this width
|
||||
}
|
||||
assert_eq!(lines[0], "Deed 2016-", "the first wrap breaks after the hyphen: {lines:?}");
|
||||
assert_eq!(lines[1], "2022", "the range continues after it: {lines:?}");
|
||||
return;
|
||||
}
|
||||
panic!("no width in the sweep wrapped the paragraph");
|
||||
}
|
||||
|
||||
/// #2712 scopes the new break to U+002D: a non-breaking hyphen (U+2011) and a minus sign
|
||||
/// (U+2212) gain no break, so the first wrap moves the range whole, breaking at the space.
|
||||
#[test]
|
||||
fn a_non_breaking_hyphen_and_a_minus_sign_stay_unbreakable() {
|
||||
for (token, hyphen) in [("2016\u{2011}2022", '\u{2011}'), ("2016\u{2212}2022", '\u{2212}')] {
|
||||
let text = format!("Deed {token}");
|
||||
for m in 100..=540 {
|
||||
let mut d = Document::from_text(&text);
|
||||
d.last_section.margin_right = m as f32;
|
||||
let l = lay(&d);
|
||||
let lines: Vec<String> = l.pages[0]
|
||||
.items
|
||||
.iter()
|
||||
.filter_map(|it| {
|
||||
if let Placed::Lines { para, l0, l1, .. } = it {
|
||||
Some(para.lines[*l0..*l1].iter().map(|line| text[line.start..line.stop].to_string()).collect::<Vec<_>>())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
.collect();
|
||||
if lines.len() < 2 {
|
||||
continue; // still fits on one line at this width
|
||||
}
|
||||
// The first wrap breaks at the space: the protected hyphen is not a break. (Narrower
|
||||
// widths fall into the break-anywhere fallback that splits too-long words between
|
||||
// characters, which the sweep leaves out, as the issue does.)
|
||||
assert_eq!(lines[0], "Deed ", "the only break is at the space, not after {hyphen}: {lines:?}");
|
||||
assert_eq!(lines[1], token, "the range stays whole: {lines:?}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ Exact line spacing keeps the run font baseline around tall equations; automatic
|
||||
| 2026-10-16 | trivial | Split-cell content retains its resolved top margin and border band on continuation pages, matching original Word Mac16.113.2 controls. Synthetic regressions cover margins, borders, line spacing and image frames. |
|
||||
| Hint-reliant CJK glyph outlines (DFKai-SB / 標楷體) | 🟡 | Required TrueType interpreter runs at units-per-em size; local Windows glyph-bounds regression and synthetic DOCX page/window comparison pass. Other platforms and hint-reliant families remain unmeasured; no native Word comparison. | — |
|
||||
| Subpoint table grid columns | 🟡 | Preserve positive grid widths rather than imposing a text-box minimum; ECMA-376 §17.4.16. Native Word for Mac 16.113.2 build26092012 confirms narrow trailing grid units. `layout::tests::subpoint_grid_keeps_widths`, `tiny_grid_scales_to_width`. Tiny totals normalize without overflowing the scale factor. Zero and invalid widths retain their fallback. Border painting remains separate. | n/a |
|
||||
| Line breaking (UAX #14 with Word's exceptions: no break after `/`) | ✅ | `layout::tests::no_line_break_right_after_a_slash` | — |
|
||||
| Line breaking (UAX #14 with Word's exceptions: no break after `/`, a break after `-` before a digit) | ✅ | `layout::tests::no_line_break_right_after_a_slash`, `layout::tests::a_hyphen_before_digits_is_a_break_opportunity_like_word` | — |
|
||||
| A break before a slash that starts a word (" /word" wraps whole; not after a no-break space) | 🟡 | `layout::tests::a_word_after_a_slash_wraps_whole`, `layout::tests::a_no_break_space_before_a_slash_still_holds`; behaviour from the #2963 report, not compared with Word yet | — |
|
||||
| Narrow table cells | ✅ | `layout::tests::narrow_cells_keep_their_width`: usable widths below 12 pt wrap without expansion; oversized glyphs still make progress | 0 |
|
||||
| Justification, Word 2013+ space shrinking (compat mode 15) | ✅ | #109 | — |
|
||||
@@ -218,6 +218,7 @@ Page-relative footer tables keep their physical wrap bounds without adding to fo
|
||||
|---|---|---|
|
||||
| 2026-10-19 | trivial | A right, centre or decimal tab whose text doesn't fit, or a left tab past the right indent, starts the next line with its text instead of ending the line (a TOC entry's page number keeps its leader); a left tab still past the right indent there takes no room (#1840) |
|
||||
| 2026-10-17 | trivial | A float above its anchor whose wrap pushes the anchor onto the next page settles with no text under it on any page (#2048 review) |
|
||||
| 2026-10-17 | trivial | A line breaks after a hyphen before a digit, like Word (#2712) |
|
||||
| 2026-10-15 | trivial | A float placed above its anchor paragraph (fixed on the page, or moved up from it) has the text of the paragraphs above the anchor on its page wrap around it, as in Word, instead of running behind it (#2048) |
|
||||
| 2026-10-18 | trivial | A word after a slash (" /word") wraps whole instead of splitting between two letters (#2963) |
|
||||
| 2026-10-15 | trivial | An at-least table row with less room than its minimum on a page that holds something starts the next page whole, as checked in Word (#1742); the taller-than-a-page case (it then splits from the top of that page) and repeated header rows (they go with it) follow the issue and synthetic tests, not a Word check |
|
||||
|
||||
Reference in new issue
Block a user