Repository navigation
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
Open
gravitypdf-bot wants to merge 2 commits into
gravitypdf-bot wants to merge 2 commits into
Conversation
…#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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-wrapandword-wrap. With #722 and this PR, #607 is complete.Problem
mPDF ignored
word-break,overflow-wrapandword-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 asoverflow-wrap: anywhere. A table cell's minimum width was always its longest word, so a column widened to hold a long URL even underword-break: break-all. CJK text broke between any two letters, even underkeep-all.div { width: 30mm }, a long word, neither property setdiv { width: 30mm; word-break: keep-all }, the same worddiv { width: 30mm; overflow-wrap: normal }overflow-wrap: break-word/anywhere,word-wrap: break-wordword-break: break-all,see <URL> nowsee https://exampl…, every line fullseealone, then the URL cutsee https://exampl…, every line fulltd { word-break: break-all }in a 30mm table, a 52-letter wordtd { overflow-wrap: anywhere }td { overflow-wrap: break-word }word-break: keep-allFix
Everything here is gated on
CssMode::STANDARD.NormalizePropertieskeepsWORD-BREAKandOVERFLOW-WRAPonly when the value is one they take.word-wrapis read asoverflow-wrap, through the same alias tablebreak-beforeuses, so a CSS-wide keyword reaches it too.setCSS()keeps each one intextparam, which already carrieshyphensinto each chunk of text and each cell's text buffer.ComputedValues, from the newInheritedProperties::WORD_BREAKlist. That list is apart fromTEXTbecause no legacy channel hands them on.CssWideKeywordsgives both an initial value ofnormal.Mpdf\Css\WordBreak(new) decides fromtextparamwhere a line may break:breaksAll(),keepsAll(),breaksLongWords()andnarrowsMinContent().WriteFlowingBlock()break-allbreaks where the line is full, unless the character before is a space.keep-alldrops 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.overflowinginflowingBlockAttr. 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.WriteFlowingBlock()andfinishFlowingBlock()). 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 inbreak-all,overflow-wrap: anywhereorword-break: break-wordis measured letter by letter, so the column can be as narrow as its widest letter. Underkeep-all, CJK text without spaces is measured as one word, not letter by letter.Decisions
word-break: normalorkeep-allon its own lets a word overflow, becauseoverflow-wrapis thennormal. A CSS-wide keyword counts as setting the property:overflow-wrap: initialoverflows.word-break: break-wordis read as the spec defines it:word-break: normalwithoverflow-wrap: anywhere. So it also narrows a cell's minimum width.keep-alland punctuation. CSS keeps breaks next to punctuation underkeep-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.text-alignsays, as CSS Text 3 §7.1 asks, sotext-align: rightorcenterdoes not push it out of the left of the box.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-wrapstaysword-wrap),setCSS()does not read them, and sotextparamnever holds them. Every new branch in the line code depends on those keys or on theoverflowingflag, which only the new path sets.WordBreakTest::testLegacyIgnoresThePropertieschecks 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-phraseis dropped as an invalid value.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
InheritedProperties::names()(Read the text-decoration longhands and text-align start, end and match-parent (#607) #722 addsDECORATION, this addsWORD_BREAK),CssWideKeywords::$initial, thesetCSS()switch next toHYPHENS, andCHANGELOG-GRAVITYPDF.md. Each one is a matter of keeping both sides.Tests
tests/Mpdf/WordBreakTest.php:word-break,overflow-wrapandword-wrap, and under neither, as overflowing, cut or filling the line;inherit; a sibling unaffected;break-all,keep-allandkeep-allwithanywhere, and a cell widened bykeep-all;WordBreakpredicates; legacy ignoring all of it.Snapshot
word-break:overflow-wrap: normal,keep-all,break-word,word-wrap: anywhere,break-all, right-aligned overflow and rtl overflow;break-word,anywhereandbreak-all, then CJK text by default, underkeep-all, and underkeep-allwithanywhere.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
composer test: OK, 12831 tests, 31 skipped (none of them snapshots)composer cs: clean--memory-limit=2G): 23 errors, all of them already on the base branch (Regenerate the PHPStan baseline for PHPStan 2.3, so the Lint job passes again #716 regenerates the baseline). None are in the changed files.🤖 Generated with Claude Code