Skip to content

Break or keep long words and CJK text by word-break and overflow-wrap (#607) - #735

Open
gravitypdf-bot wants to merge 2 commits into
feature/608-vertical-align-text-indentfrom
feature/607-word-break
Open

gravitypdf-bot wants to merge 2 commits into
feature/608-vertical-align-text-indentfrom
feature/607-word-break

Conversation

@gravitypdf-bot

Copy link
Copy Markdown

Part of #607
Part of #552, part of #520
Stacked on #731

This is the third part of #607, the part #722 left out: word-break, overflow-wrap and word-wrap. With #722 and this PR, #607 is complete.

Problem

mPDF ignored word-break, overflow-wrap and word-wrap. WriteFlowingBlock() tried its own break opportunities in order. When it found none, it cut the word where the line was full, so every document behaved as overflow-wrap: anywhere. A table cell's minimum width was always its longest word, so a column widened to hold a long URL even under word-break: break-all. CJK text broke between any two letters, even under keep-all.

$mpdf = new \Mpdf\Mpdf();
$mpdf->WriteHTML('
<div style="width: 30mm; word-break: keep-all">Supercalifragilisticexpialidocious</div>
<div style="width: 30mm; word-break: break-all">see https://example.com/documents/annual-report-2026.pdf now</div>
<table style="width: 30mm"><tr><td style="word-break: break-all">ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ</td></tr></table>');
Rule Browser Before After (standard)
div { width: 30mm }, a long word, neither property set overflows cut at the edge cut at the edge (unchanged, see Decisions)
div { width: 30mm; word-break: keep-all }, the same word overflows cut into pieces overflows
div { width: 30mm; overflow-wrap: normal } overflows cut overflows
overflow-wrap: break-word / anywhere, word-wrap: break-word cut at the edge cut cut
word-break: break-all, see <URL> now see https://exampl…, every line full see alone, then the URL cut see https://exampl…, every line full
td { word-break: break-all } in a 30mm table, a 52-letter word the cell stays 30mm and the word breaks the table widens to 136mm the cell stays 30mm and the word breaks
td { overflow-wrap: anywhere } the same widens the cell stays 30mm
td { overflow-wrap: break-word } the table widens widens widens
CJK text, word-break: keep-all breaks only at spaces and punctuation breaks between letters breaks only at spaces and punctuation

Fix

Everything here is gated on CssMode::STANDARD.

  • Reading. NormalizeProperties keeps WORD-BREAK and OVERFLOW-WRAP only when the value is one they take. word-wrap is read as overflow-wrap, through the same alias table break-before uses, so a CSS-wide keyword reaches it too. setCSS() keeps each one in textparam, which already carries hyphens into each chunk of text and each cell's text buffer.
  • Inheritance. Both are inherited through ComputedValues, from the new InheritedProperties::WORD_BREAK list. That list is apart from TEXT because no legacy channel hands them on. CssWideKeywords gives both an initial value of normal.
  • Mpdf\Css\WordBreak (new) decides from textparam where a line may break: breaksAll(), keepsAll(), breaksLongWords() and narrowsMinContent().
  • WriteFlowingBlock()
    • break-all breaks where the line is full, unless the character before is a space.
    • keep-all drops the break between two CJK characters (step 7) unless one of them is CJK punctuation (WordBreak::CJK_PUNCTUATION). It also drops Oikomi, which squeezes a letter onto the line and then breaks after it. Hanging punctuation is kept.
    • When no break is found and the properties do not allow cutting the word, the character goes on the line and the word overflows. The line is flagged overflowing in flowingBlockAttr. While the word overflows, each new character searches back only to where the previous search started, so a long URL is not searched again from the start of the line for every character.
  • Drawing an overflowing line (WriteFlowingBlock() and finishFlowingBlock()). The line is not squeezed with negative character spacing, which is what mPDF does to a line wider than its box, and it is not justified. It is aligned to its start, as CSS Text says: from the left of an ltr block, and from the right of an rtl one, running out of its left.
  • TableCheckMinWidth(). A chunk set in break-all, overflow-wrap: anywhere or word-break: break-word is measured letter by letter, so the column can be as narrow as its widest letter. Under keep-all, CJK text without spaces is measured as one word, not letter by letter.

Decisions

  • Default when neither property is set. On paper an overflowing word is lost, so the word is still cut at the edge when no element sets either property, as the issue asks. Once either is set, CSS is followed. So word-break: normal or keep-all on its own lets a word overflow, because overflow-wrap is then normal. A CSS-wide keyword counts as setting the property: overflow-wrap: initial overflows.
  • word-break: break-word is read as the spec defines it: word-break: normal with overflow-wrap: anywhere. So it also narrows a cell's minimum width.
  • keep-all and punctuation. CSS keeps breaks next to punctuation under keep-all. A CJK break stays allowed when the character before or after it is CJK punctuation (ideographic comma and full stop, brackets, fullwidth and compatibility forms). The usual rules about which characters may not start or end a line still apply.
  • An overflowing line is start-aligned whatever text-align says, as CSS Text 3 §7.1 asks, so text-align: right or center does not push it out of the left of the box.
  • An invalid value (word-break: sometimes) is dropped. As with every property in mPDF, a later invalid declaration in the same block still hides an earlier valid one, because the parser keeps one value per property before it is normalised.

Legacy mode

Unchanged. The declarations are not normalised (word-wrap stays word-wrap), setCSS() does not read them, and so textparam never holds them. Every new branch in the line code depends on those keys or on the overflowing flag, which only the new path sets. WordBreakTest::testLegacyIgnoresTheProperties checks that every line of a block and a cell under each value is drawn as without it. No existing snapshot fixture changed. Nothing here only fixes a crash, so nothing applies in both modes.

Not in this PR

  • word-break: auto-phrase is dropped as an invalid value.
  • Under keep-all, a cell's minimum width takes a run of CJK text without spaces as one word, including any punctuation inside it, where a browser could break after the punctuation.

Expected conflicts

Tests

  • tests/Mpdf/WordBreakTest.php:

    • a long URL in a 30mm block under each value of word-break, overflow-wrap and word-wrap, and under neither, as overflowing, cut or filling the line;
    • the same in a 30mm table cell, narrowed or widened;
    • inheritance into a child block, an inline element, a table's cells and a cell's child block; inherit; a sibling unaffected;
    • an overflowing line keeps its natural width and starts at the start of the line, for left, justified, right and centred text and for rtl;
    • CJK text by default, under break-all, keep-all and keep-all with anywhere, and a cell widened by keep-all;
    • normalisation of the declarations; the WordBreak predicates; legacy ignoring all of it.
  • Snapshot word-break:

    • page 1: a URL in a 30mm block under neither property, overflow-wrap: normal, keep-all, break-word, word-wrap: anywhere, break-all, right-aligned overflow and rtl overflow;
    • pages 1–2: a URL in a 30mm table cell under neither property, break-word, anywhere and break-all, then CJK text by default, under keep-all, and under keep-all with anywhere.

    Each case sits under a caption saying what it should show. I rasterised the fixture and checked each case against its caption. Against the pre-fix src/ it fails (page 1 RMSE 0.114, page 2 RMSE 0.105).

Checks

🤖 Generated with Claude Code

gravitypdf-bot and others added 2 commits October 12, 2026 08:20
…#607)

mPDF ignored word-break, overflow-wrap and word-wrap, and always cut a word
too long for its line where the line was full. In a table cell the column
widened to hold the longest word, whatever the style said.

Under cssMode standard both properties are now read and inherited, with
word-wrap as overflow-wrap. With neither set, a long word is still cut at the
edge, the better default on paper. Once either is set, the word overflows the
line unless they let it break: break-all fills each line to the edge,
break-word and anywhere cut the word at the edge, and keep-all stops CJK text
breaking between its letters. An overflowing line keeps its width and starts
at the start of the line. In a table cell break-all, anywhere and
word-break: break-word narrow the column to its widest letter.

Legacy mode ignores the properties as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h WordBreak (#607)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant