Skip to content

Commit

Permalink
#429 More correct implementation of break-word.
Browse files Browse the repository at this point in the history
Take two, with additional tests.
  • Loading branch information
danfickle committed Jan 28, 2020
1 parent 816193a commit 1bfedae
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 37 deletions.
114 changes: 82 additions & 32 deletions openhtmltopdf-core/src/main/java/com/openhtmltopdf/layout/Breaker.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import com.openhtmltopdf.css.style.CalculatedStyle;
import com.openhtmltopdf.css.style.CssContext;
import com.openhtmltopdf.extend.FSTextBreaker;
import com.openhtmltopdf.layout.LineBreakContext.LineBreakResult;
import com.openhtmltopdf.render.FSFont;

/**
Expand Down Expand Up @@ -71,7 +72,7 @@ private static int getFirstLetterEnd(String text, int start) {
return end;
}

public static void breakText(LayoutContext c,
public static LineBreakResult breakText(LayoutContext c,
LineBreakContext context, int avail,
CalculatedStyle style, boolean tryToBreakAnywhere, int lineWidth) {

Expand All @@ -84,7 +85,7 @@ public static void breakText(LayoutContext c,
if (whitespace == IdentValue.NOWRAP) {
context.setEnd(context.getLast());
context.setWidth(Breaker.getTextWidthWithLetterSpacing(c, font, context.getCalculatedSubstring(), letterSpacing));
return;
return LineBreakResult.WORD_BREAKING_FINISHED;
}

//check if we should break on the next newline
Expand All @@ -106,54 +107,75 @@ public static void breakText(LayoutContext c,
//check if we may wrap
if (whitespace == IdentValue.PRE ||
(context.isNeedsNewLine() && context.getWidth() <= avail)) {
return;
return context.isNeedsNewLine() ?
LineBreakResult.WORD_BREAKING_NEED_NEW_LINE :
LineBreakResult.WORD_BREAKING_FINISHED;
}

context.setEndsOnNL(false);

if (style.getWordWrap() != IdentValue.BREAK_WORD) {
// Ordinary old word wrap which will overflow too long unbreakable words.
doBreakText(c, context, avail, style, tryToBreakAnywhere);
return doBreakText(c, context, avail, style, tryToBreakAnywhere);
} else {
int originalStart = context.getStart();

// The idea is we only break a word if it will not fit on a line by itself.

LineBreakResult result;
LOOP:
while (true) {
doBreakText(c, context, avail, style, tryToBreakAnywhere);
result = doBreakText(c, context, avail, style, tryToBreakAnywhere);

switch (result) {
case WORD_BREAKING_FINISHED:
case CHAR_BREAKING_FINISHED:
case CHAR_BREAKING_UNBREAKABLE:
case CHAR_BREAKING_NEED_NEW_LINE:
break LOOP;

if (context.isFinished()) {
break;
} else if (tryToBreakAnywhere && context.isEndsOnWordBreak()) {
// We were in char breaking mode, but have found a line breaking opportunity.
case CHAR_BREAKING_FOUND_WORD_BREAK:
tryToBreakAnywhere = false;
} else if (!tryToBreakAnywhere && context.isNeedsNewLine() && context.getNextWidth() >= lineWidth) {
// The next word will not fit on a line by itself so turn on char breaking mode.
tryToBreakAnywhere = true;
} else if (!tryToBreakAnywhere && context.isUnbreakable()) {
// Safety valve: Not sure we need it.
break;
} else if (context.isNeedsNewLine()) {
// Stop, we're at the end of the line.
break;

case WORD_BREAKING_NEED_NEW_LINE: {
if (context.getNextWidth() >= lineWidth) {
tryToBreakAnywhere = true;
break;
} else {
break LOOP;
}
}
case WORD_BREAKING_UNBREAKABLE: {
if (context.getWidth() >= lineWidth) {
tryToBreakAnywhere = true;
context.resetEnd();
continue LOOP;
} else {
break LOOP;
}
}

default:
break LOOP;
}

avail -= context.getWidth();
context.setStart(context.getEnd());
avail -= context.getWidth();
}

context.setStart(originalStart);

// We need to know this for the next line.
context.setFinishedInCharBreakingMode(tryToBreakAnywhere);
return result;
}
}

private static void doBreakText(LayoutContext c,
private static LineBreakResult doBreakText(LayoutContext c,
LineBreakContext context, int avail, CalculatedStyle style,
boolean tryToBreakAnywhere) {
if (!tryToBreakAnywhere) {
doBreakText(c, context, avail, style, STANDARD_LINE_BREAKER);
return doBreakText(c, context, avail, style, STANDARD_LINE_BREAKER);
} else {
FSFont font = style.getFSFont(c);

Expand All @@ -168,15 +190,15 @@ private static void doBreakText(LayoutContext c,
FSTextBreaker lineIterator = STANDARD_LINE_BREAKER.getBreaker(currentString, c.getSharedContext());
FSTextBreaker charIterator = STANDARD_CHARACTER_BREAKER.getBreaker(currentString, c.getSharedContext());

doBreakCharacters(currentString, lineIterator, charIterator, context, avail, letterSpacing, measurer);
return doBreakCharacters(currentString, lineIterator, charIterator, context, avail, letterSpacing, measurer);
}
}

/**
* Breaks at most one word (until the next word break) going character by character to see
* what will fit in.
*/
static void doBreakCharacters(
static LineBreakResult doBreakCharacters(
String currentString,
FSTextBreaker lineIterator,
FSTextBreaker charIterator,
Expand Down Expand Up @@ -237,10 +259,19 @@ static void doBreakCharacters(

if (graphicsLength == avail) {
// Exact fit..
context.setNeedsNewLine(currentString.length() > left);
context.setEnd(left);
boolean needNewLine = currentString.length() > left;

context.setNeedsNewLine(needNewLine);
context.setEnd(left + context.getStart());
context.setWidth(graphicsLength);
return;

if (left >= currentString.length()) {
return LineBreakResult.CHAR_BREAKING_FINISHED;
} else if (left >= nextWordBreak) {
return LineBreakResult.CHAR_BREAKING_FOUND_WORD_BREAK;
} else {
return LineBreakResult.CHAR_BREAKING_NEED_NEW_LINE;
}
}

if (nextCharBreak < 0) {
Expand All @@ -252,7 +283,7 @@ static void doBreakCharacters(
lastGoodWrap = nextCharBreak;
lastGoodGraphicsLength = graphicsLength;

nextCharBreak = Math.min(currentString.length(), nextWordBreak);
nextCharBreak = nextWordBreak;

float extraSpacing = (nextCharBreak - left) * letterSpacing;
int splitWidth = (int) (measurer.applyAsInt(currentString.substring(left, nextCharBreak)) + extraSpacing);
Expand All @@ -265,7 +296,14 @@ static void doBreakCharacters(
context.setWidth(graphicsLength);
context.setEnd(nextCharBreak + context.getStart());
context.setEndsOnWordBreak(nextCharBreak == nextWordBreak);
return;

if (nextCharBreak >= currentString.length()) {
return LineBreakResult.CHAR_BREAKING_FINISHED;
} else if (nextCharBreak >= nextWordBreak) {
return LineBreakResult.CHAR_BREAKING_FOUND_WORD_BREAK;
} else {
return LineBreakResult.CHAR_BREAKING_NEED_NEW_LINE;
}
}

// We need a newline for this word.
Expand All @@ -276,7 +314,14 @@ static void doBreakCharacters(
context.setWidth(lastGoodGraphicsLength);
context.setEnd(lastGoodWrap + context.getStart());
context.setEndsOnWordBreak(lastGoodWrap == nextWordBreak);
return;

if (lastGoodWrap >= currentString.length()) {
return LineBreakResult.CHAR_BREAKING_FINISHED;
} else if (lastGoodWrap >= nextWordBreak) {
return LineBreakResult.CHAR_BREAKING_FOUND_WORD_BREAK;
} else {
return LineBreakResult.CHAR_BREAKING_NEED_NEW_LINE;
}
} else {
// One character word, so we didn't find a wrap point.
float extraSpacing = nextWordBreak * letterSpacing;
Expand All @@ -286,7 +331,8 @@ static void doBreakCharacters(
context.setEnd(nextWordBreak + context.getStart());
context.setEndsOnWordBreak(true);
context.setWidth(splitWidth);
return;

return LineBreakResult.CHAR_BREAKING_UNBREAKABLE;
}
}

Expand All @@ -308,7 +354,7 @@ void copyTo(AppBreakOpportunity other) {
}
}

public static void doBreakText(
public static LineBreakResult doBreakText(
LayoutContext c,
LineBreakContext context,
int avail,
Expand Down Expand Up @@ -387,7 +433,7 @@ public static void doBreakText(
context.setWidth(current.graphicsLength);
context.setEnd(context.getMaster().length());
// It all fit!
return;
return LineBreakResult.WORD_BREAKING_FINISHED;
}

context.setNeedsNewLine(true);
Expand All @@ -403,6 +449,8 @@ public static void doBreakText(

context.setNextWidth(nextUnfittableSplitWidth);
context.setEnd(context.getStart() + lastWrap);

return LineBreakResult.WORD_BREAKING_NEED_NEW_LINE;
} else {
// Unbreakable string
if (current.left == 0) {
Expand All @@ -420,6 +468,8 @@ public static void doBreakText(
} else {
context.setWidth(current.graphicsLength);
}

return LineBreakResult.WORD_BREAKING_UNBREAKABLE;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,10 +186,11 @@ public static void layoutContent(LayoutContext c, BlockBox box, int initialY, in
continue;
}
} else {
if (!startInlineText(c, lbContext, inlineBox, space, current, fit, trimmedLeadingSpace, inCharBreakingMode)) {
boolean shouldContinue = !startInlineText(c, lbContext, inlineBox, space, current, fit, trimmedLeadingSpace, inCharBreakingMode);
inCharBreakingMode = lbContext.isFinishedInCharBreakingMode();
if (shouldContinue) {
continue;
}
inCharBreakingMode = lbContext.isFinishedInCharBreakingMode();
}
}

Expand Down Expand Up @@ -374,14 +375,15 @@ private static boolean startInlineText(
boolean trimmedLeadingSpace, boolean tryToBreakAnywhere) {

lbContext.saveEnd();
CalculatedStyle style = inlineBox.getStyle();

// Layout the text into the remaining width on this line. Will only go to the end of the line (at most)
// and will produce one InlineText object.
InlineText inlineText = layoutText(
c, inlineBox.getStyle(), space.remainingWidth - fit, lbContext, false, inlineBox.getTextDirection(), tryToBreakAnywhere, space.maxAvailableWidth - fit);
c, style, space.remainingWidth - fit, lbContext, false, inlineBox.getTextDirection(), tryToBreakAnywhere, space.maxAvailableWidth - fit);

if (inlineBox.getStyle().hasLetterSpacing()) {
inlineText.setLetterSpacing(inlineBox.getStyle().getFloatPropertyProportionalWidth(CSSName.LETTER_SPACING, 0, c));
if (style.hasLetterSpacing()) {
inlineText.setLetterSpacing(style.getFloatPropertyProportionalWidth(CSSName.LETTER_SPACING, 0, c));
}

if (lbContext.isUnbreakable() && !current.line.isContainsContent()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,20 @@
* to the layout code.
*/
public class LineBreakContext {
public static enum LineBreakResult {
CHAR_BREAKING_NEED_NEW_LINE,
WORD_BREAKING_NEED_NEW_LINE,

CHAR_BREAKING_UNBREAKABLE,
WORD_BREAKING_UNBREAKABLE,

CHAR_BREAKING_FOUND_WORD_BREAK,

CHAR_BREAKING_FINISHED,
WORD_BREAKING_FINISHED;
}


private String _master;
private int _start;
private int _end;
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<html>
<head>
<style>
@page {
size: 100mm 25cm;
}
body, td {
border: 1px solid black;
}
table, .wrap {
word-wrap: break-word;
}
</style>
</head>
<body style="font-family: monospace;">

<div class="wrap">
FirstWordTooLongForLineSoItShouldWrap MiddleWordTooLongForLineSoItShouldWrap LastWordTooLongForLineSoItShouldWrap
</div><br/>

<div class="wrap">
First Word Too Long For Line So It Should MiddleWordTooLongForLineSoItShouldWrap LastWordTooLongForLineSoItShouldWrap
</div><br/>

<div class="wrap">
First Word Too Long For Line So It Should MiddleWordTooLongForLineSoItShouldWrap Last Word Too Long For Line So It Should
</div><br/>

<div class="wrap">
FirstWordNotTooLongForLineSoIt FirstWordNotTooLongForLineSoIt FirstWordNotTooLongForLineSoIt FirstWordNotTooLongForLineSoIt
</div><br/>

<div class="wrap">
FirstWordNotTooLongForLine <b>FirstWordNotTooLongForLine</b> <b><i>FirstWordNotTooLongForLineSoIt FirstWordNotTooLongForLineSoIt</i></b>
</div><br/>

<div class="wrap">
One two three FirstWordNotTooLongForLineSoIt four five FirstWordNotTooLongForLineSoIt six seven FirstWordNotTooLongForLineSoIt eight FirstWordNotTooLongForLineSoIt
</div><br/>

<table>
<tr><td>OneTwoThreeFour</td><td>Five six seven</td><td>Eight nine</td></tr>
<tr><td>One Two Three Four</td><td>Fivesixseven</td><td>Eight nine</td></tr>
<tr><td>One Two Three Four</td><td>Five six seven</td><td>Eightnine</td></tr>
<tr><td>OneTwoThreeFour</td><td>Fivesixseven</td><td>Eightnine</td></tr>
</table>

</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<html>
<head>
<style>
@page {
size: 60mm 20cm;
margin: 0;
}
body {
word-wrap: break-word;
margin: 0;
max-width: 60mm;
}
</style>
</head>
<body>

<div>
<div style="width: 50%; height: 30px; background-color: red;float: left;"></div>Some words some more.
FirstWordTooLongForLineSoItShouldWrap MiddleWordTooLongForLineSoItShouldWrap LastWordTooLongForLineSoItShouldWrap
</div>

<div style="clear:both;"></div>

<div>
<div style="width: 50%; height: 30px; background-color: red;float: right;"></div>
FirstWordTooLongForLineSoItShouldWrap MiddleWordTooLongForLineSoItShouldWrap LastWordTooLongForLineSoItShouldWrap
</div>

<div style="clear:both;"></div>

</body>
</html>
Loading

0 comments on commit 1bfedae

Please sign in to comment.