attach rules for line wrapping to both ends of IInlineBox

The LineWrappingRules allow better control of where to find possible
points to wrap inline content into the next line. The rules allow
to force a line wrapping at a certain point.

Signed-off-by: Florian Thienel <florian@thienel.org>
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/TestLineArrangement.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/TestLineArrangement.java
index 35d4535..d180a3d 100644
--- a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/TestLineArrangement.java
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/TestLineArrangement.java
@@ -43,26 +43,26 @@
 
 	@Test
 	public void givenAllBoxesFitIntoOneLine_shouldArrangeBoxesInOneLine() throws Exception {
-		lines.arrangeBoxes(graphics, joinableBoxes.listIterator(), 210, TextAlign.LEFT, false);
+		lines.arrangeBoxes(graphics, joinableBoxes.listIterator(), 210, TextAlign.LEFT);
 		assertEquals(1, lines.getLines().size());
 	}
 
 	@Test
 	public void givenJoinableBoxes_whenBoxesFitIntoSameLine_shouldJoinBoxes() throws Exception {
-		lines.arrangeBoxes(graphics, joinableBoxes.listIterator(), 210, TextAlign.LEFT, false);
+		lines.arrangeBoxes(graphics, joinableBoxes.listIterator(), 210, TextAlign.LEFT);
 		assertEquals(1, joinableBoxes.size());
 	}
 
 	@Test
 	public void givenUnjoinableBoxes_whenBoxesFitIntoSameLane_shouldNotJoinBoxes() throws Exception {
-		lines.arrangeBoxes(graphics, unjoinableBoxes.listIterator(), 210, TextAlign.LEFT, false);
+		lines.arrangeBoxes(graphics, unjoinableBoxes.listIterator(), 210, TextAlign.LEFT);
 		assertEquals(3, unjoinableBoxes.size());
 	}
 
 	@Test
 	public void givenUnjoinableBoxFollowedByJoinableBoxWithoutProperSplitPointAtLineEnd_whenAdditionalBoxWithoutProperSplitPointDoesNotFitIntoLine_shouldWrapCompleteJoinedBoxIntoNextLine() throws Exception {
 		final List<IInlineBox> boxes = boxes(square(10), staticText("L"), staticText("or"));
-		lines.arrangeBoxes(graphics, boxes.listIterator(), 18, TextAlign.LEFT, false);
+		lines.arrangeBoxes(graphics, boxes.listIterator(), 18, TextAlign.LEFT);
 
 		assertEquals(2, boxes.size());
 		assertEquals("Lor", ((StaticText) boxes.get(1)).getText());
@@ -71,7 +71,7 @@
 	@Test
 	public void givenUnjoinableBoxFollowedByJoinableBoxWithoutProperSplitPointAtLineEnd_whenAdditionalBoxWithoutProperSplitPointDoesNotFitIntoLine_shouldRemoveOriginalLastBox() throws Exception {
 		final List<IInlineBox> boxes = boxes(square(10), staticText("L"), staticText("or"));
-		lines.arrangeBoxes(graphics, boxes.listIterator(), 18, TextAlign.LEFT, false);
+		lines.arrangeBoxes(graphics, boxes.listIterator(), 18, TextAlign.LEFT);
 
 		for (final IInlineBox box : boxes) {
 			if (box.getWidth() == 0) {
@@ -86,7 +86,7 @@
 		layout(boxes);
 		final int widthOfHeadBoxes = boxes.get(0).getWidth() + boxes.get(1).getWidth();
 
-		lines.arrangeBoxes(graphics, boxes.listIterator(), widthOfHeadBoxes + 1, TextAlign.LEFT, false);
+		lines.arrangeBoxes(graphics, boxes.listIterator(), widthOfHeadBoxes + 1, TextAlign.LEFT);
 
 		assertEquals(1, lines.getLines().size());
 		assertEquals(boxes.get(2), lines.getLines().iterator().next().getLastChild());
@@ -98,7 +98,7 @@
 		layout(boxes);
 		final int widthOfHeadBoxes = boxes.get(0).getWidth() + boxes.get(1).getWidth();
 
-		lines.arrangeBoxes(graphics, boxes.listIterator(), widthOfHeadBoxes + 1, TextAlign.LEFT, false);
+		lines.arrangeBoxes(graphics, boxes.listIterator(), widthOfHeadBoxes + 1, TextAlign.LEFT);
 
 		assertEquals(1, lines.getLines().size());
 		assertEquals(boxes.get(2), lines.getLines().iterator().next().getLastChild());
@@ -110,7 +110,7 @@
 		layout(boxes);
 		final int widthOfHeadBoxes = boxes.get(0).getWidth() + boxes.get(1).getWidth();
 
-		lines.arrangeBoxes(graphics, boxes.listIterator(), widthOfHeadBoxes + 10, TextAlign.LEFT, false);
+		lines.arrangeBoxes(graphics, boxes.listIterator(), widthOfHeadBoxes + 10, TextAlign.LEFT);
 
 		assertEquals(2, lines.getLines().size());
 		assertEquals(" ", ((StaticText) lines.getLines().iterator().next().getLastChild()).getText());
@@ -123,7 +123,7 @@
 			layout(boxes);
 			final int widthOfHeadBoxes = boxes.get(0).getWidth() + boxes.get(1).getWidth();
 
-			lines.arrangeBoxes(graphics, boxes.listIterator(), widthOfHeadBoxes + x, TextAlign.LEFT, false);
+			lines.arrangeBoxes(graphics, boxes.listIterator(), widthOfHeadBoxes + x, TextAlign.LEFT);
 
 			assertEquals("x = " + x, 2, lines.getLines().size());
 			assertEquals("x = " + x, " ", ((StaticText) lines.getLines().iterator().next().getLastChild()).getText());
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/BoxFactory.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/BoxFactory.java
index f7d5806..4424362 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/BoxFactory.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/BoxFactory.java
@@ -132,25 +132,6 @@
 		return paragraph;
 	}
 
-	public static Paragraph paragraphPreservingWhitespace(final IInlineBox... children) {
-		final Paragraph paragraph = new Paragraph();
-		for (final IInlineBox child : children) {
-			paragraph.appendChild(child);
-		}
-		paragraph.setPreservingWhitespace(true);
-		return paragraph;
-	}
-
-	public static Paragraph paragraphPreservingWhitespace(final TextAlign textAlign, final IInlineBox... children) {
-		final Paragraph paragraph = new Paragraph();
-		for (final IInlineBox child : children) {
-			paragraph.appendChild(child);
-		}
-		paragraph.setTextAlign(textAlign);
-		paragraph.setPreservingWhitespace(true);
-		return paragraph;
-	}
-
 	public static InlineContainer inlineContainer(final IInlineBox... children) {
 		final InlineContainer inlineContainer = new InlineContainer();
 		for (final IInlineBox child : children) {
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IInlineBox.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IInlineBox.java
index c710f6c..7559cc0 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IInlineBox.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IInlineBox.java
@@ -28,6 +28,12 @@
 
 	int getInvisibleGapAtEnd(Graphics graphics);
 
+	LineWrappingRule getLineWrappingAtStart();
+
+	LineWrappingRule getLineWrappingAtEnd();
+
+	boolean requiresSplitForLineWrapping();
+
 	boolean canJoin(IInlineBox other);
 
 	boolean join(IInlineBox other);
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/InlineContainer.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/InlineContainer.java
index 02a891a..4e0cdb8 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/InlineContainer.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/InlineContainer.java
@@ -25,6 +25,8 @@
 	private int width;
 	private int height;
 	private int baseline;
+	private boolean containsChildThatRequiresLineWrapping;
+
 	private final LinkedList<IInlineBox> children = new LinkedList<IInlineBox>();
 
 	@Override
@@ -103,6 +105,27 @@
 	}
 
 	@Override
+	public LineWrappingRule getLineWrappingAtStart() {
+		if (children.isEmpty()) {
+			return LineWrappingRule.ALLOWED;
+		}
+		return children.getFirst().getLineWrappingAtStart();
+	}
+
+	@Override
+	public LineWrappingRule getLineWrappingAtEnd() {
+		if (children.isEmpty()) {
+			return LineWrappingRule.ALLOWED;
+		}
+		return children.getLast().getLineWrappingAtEnd();
+	}
+
+	@Override
+	public boolean requiresSplitForLineWrapping() {
+		return containsChildThatRequiresLineWrapping;
+	}
+
+	@Override
 	public void accept(final IBoxVisitor visitor) {
 		visitor.visit(this);
 	}
@@ -161,11 +184,13 @@
 		layoutChildren(graphics);
 		calculateBoundsAndBaseline();
 		arrangeChildrenOnBaseline();
+		updateRequiresSplitForLineWrapping();
 	}
 
 	private void layoutChildren(final Graphics graphics) {
 		for (final IInlineBox child : children) {
 			child.layout(graphics);
+			containsChildThatRequiresLineWrapping |= child.requiresSplitForLineWrapping();
 		}
 	}
 
@@ -191,6 +216,13 @@
 		}
 	}
 
+	private void updateRequiresSplitForLineWrapping() {
+		containsChildThatRequiresLineWrapping = false;
+		for (final IInlineBox child : children) {
+			containsChildThatRequiresLineWrapping |= child.requiresSplitForLineWrapping();
+		}
+	}
+
 	@Override
 	public boolean reconcileLayout(final Graphics graphics) {
 		final int oldWidth = width;
@@ -282,11 +314,21 @@
 	}
 
 	private int findChildIndexToSplitAt(final int headWidth) {
-		for (int i = 0; i < children.size(); i += 1) {
-			final IInlineBox child = children.get(i);
+		int i = 0;
+		for (final IInlineBox child : children) {
+			if (child.getLineWrappingAtStart() == LineWrappingRule.REQUIRED && i > 0) {
+				return i - 1;
+			}
+			if (child.getLineWrappingAtEnd() == LineWrappingRule.REQUIRED) {
+				return i;
+			}
+			if (child.requiresSplitForLineWrapping()) {
+				return i;
+			}
 			if (child.getLeft() + child.getWidth() > headWidth) {
 				return i;
 			}
+			i += 1;
 		}
 		return -1;
 	}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/InlineFrame.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/InlineFrame.java
index 56147af..3639f5b 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/InlineFrame.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/InlineFrame.java
@@ -107,6 +107,30 @@
 	}
 
 	@Override
+	public LineWrappingRule getLineWrappingAtStart() {
+		if (component == null) {
+			return LineWrappingRule.ALLOWED;
+		}
+		return component.getLineWrappingAtStart();
+	}
+
+	@Override
+	public LineWrappingRule getLineWrappingAtEnd() {
+		if (component == null) {
+			return LineWrappingRule.ALLOWED;
+		}
+		return component.getLineWrappingAtEnd();
+	}
+
+	@Override
+	public boolean requiresSplitForLineWrapping() {
+		if (component == null) {
+			return false;
+		}
+		return component.requiresSplitForLineWrapping();
+	}
+
+	@Override
 	public Rectangle getBounds() {
 		return new Rectangle(left, top, width, height);
 	}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/InlineNodeReference.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/InlineNodeReference.java
index 10a472b..9bba511 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/InlineNodeReference.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/InlineNodeReference.java
@@ -113,6 +113,30 @@
 	}
 
 	@Override
+	public LineWrappingRule getLineWrappingAtStart() {
+		if (component == null) {
+			return LineWrappingRule.ALLOWED;
+		}
+		return component.getLineWrappingAtStart();
+	}
+
+	@Override
+	public LineWrappingRule getLineWrappingAtEnd() {
+		if (component == null) {
+			return LineWrappingRule.ALLOWED;
+		}
+		return component.getLineWrappingAtEnd();
+	}
+
+	@Override
+	public boolean requiresSplitForLineWrapping() {
+		if (component == null) {
+			return false;
+		}
+		return component.requiresSplitForLineWrapping();
+	}
+
+	@Override
 	public Rectangle getBounds() {
 		return new Rectangle(left, top, width, height);
 	}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/LineArrangement.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/LineArrangement.java
index 6571e70..772a68c 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/LineArrangement.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/LineArrangement.java
@@ -27,7 +27,7 @@
 	private boolean lastBoxWrappedCompletely;
 	private Line currentLine;
 
-	public void arrangeBoxes(final Graphics graphics, final ListIterator<IInlineBox> boxIterator, final int width, final TextAlign textAlign, final boolean preservingWhitespace) {
+	public void arrangeBoxes(final Graphics graphics, final ListIterator<IInlineBox> boxIterator, final int width, final TextAlign textAlign) {
 		this.boxIterator = boxIterator;
 		this.width = width;
 		reset();
@@ -53,7 +53,7 @@
 		final boolean boxWrappedCompletely;
 		if (currentLine.canJoinWithLastChild(box)) {
 			boxWrappedCompletely = arrangeWithLastChild(graphics, box);
-		} else if (boxFitsIntoCurrentLine(box) || !hasVisibleContent(box)) {
+		} else if ((boxFitsIntoCurrentLine(box) || !hasVisibleContent(box)) && !box.requiresSplitForLineWrapping()) {
 			boxWrappedCompletely = appendToCurrentLine(box);
 		} else if (box.canSplit()) {
 			boxWrappedCompletely = splitAndWrapToNextLine(graphics, box);
@@ -120,6 +120,10 @@
 	}
 
 	private boolean splitAndWrapToNextLine(final Graphics graphics, final IInlineBox box) {
+		if (box.getLineWrappingAtStart() == LineWrappingRule.REQUIRED) {
+			return wrapCompletelyToNextLine(box);
+		}
+
 		final int headWidth = width - currentLine.getWidth();
 		final IInlineBox tail = box.splitTail(graphics, headWidth, !currentLine.hasChildren());
 		final boolean boxWrappedCompletely = box.getWidth() == 0;
@@ -128,9 +132,12 @@
 		} else {
 			currentLine.appendChild(box);
 		}
+
 		if (tail.getWidth() > 0) {
 			insertNextBox(tail);
 			lineBreak();
+		} else if (box.getLineWrappingAtEnd() == LineWrappingRule.REQUIRED) {
+			lineBreak();
 		}
 
 		return boxWrappedCompletely;
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/LineWrappingRule.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/LineWrappingRule.java
new file mode 100644
index 0000000..6fb023d
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/LineWrappingRule.java
@@ -0,0 +1,18 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Florian Thienel and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * 		Florian Thienel - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.vex.core.internal.boxes;
+
+/**
+ * @author Florian Thienel
+ */
+public enum LineWrappingRule {
+	ALLOWED, REQUIRED;
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/Paragraph.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/Paragraph.java
index 3dd8f8a..492658c 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/Paragraph.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/Paragraph.java
@@ -29,7 +29,6 @@
 	private int width;
 
 	private TextAlign textAlign = TextAlign.LEFT;
-	private boolean preservingWhitespace = false;
 
 	private final LinkedList<IInlineBox> children = new LinkedList<IInlineBox>();
 	private final LineArrangement lines = new LineArrangement();
@@ -104,14 +103,6 @@
 		this.textAlign = textAlign;
 	}
 
-	public boolean isPreservingWhitespace() {
-		return preservingWhitespace;
-	}
-
-	public void setPreservingWhitespace(final boolean preservingWhitespace) {
-		this.preservingWhitespace = preservingWhitespace;
-	}
-
 	@Override
 	public void accept(final IBoxVisitor visitor) {
 		visitor.visit(this);
@@ -169,7 +160,7 @@
 	}
 
 	private void arrangeChildrenOnLines(final Graphics graphics) {
-		lines.arrangeBoxes(graphics, children.listIterator(), width, textAlign, preservingWhitespace);
+		lines.arrangeBoxes(graphics, children.listIterator(), width, textAlign);
 	}
 
 	@Override
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/Square.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/Square.java
index fc4f5f8..99d75a2 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/Square.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/Square.java
@@ -25,6 +25,9 @@
 	private int left;
 	private int size;
 
+	private LineWrappingRule lineWrappingAtStart = LineWrappingRule.ALLOWED;
+	private LineWrappingRule lineWrappingAtEnd = LineWrappingRule.ALLOWED;
+
 	@Override
 	public void setParent(final IBox parent) {
 		this.parent = parent;
@@ -102,6 +105,29 @@
 	}
 
 	@Override
+	public LineWrappingRule getLineWrappingAtStart() {
+		return lineWrappingAtStart;
+	}
+
+	public void setLineWrappingAtStart(final LineWrappingRule wrappingRule) {
+		lineWrappingAtStart = wrappingRule;
+	}
+
+	@Override
+	public LineWrappingRule getLineWrappingAtEnd() {
+		return lineWrappingAtEnd;
+	}
+
+	public void setLineWrappingAtEnd(final LineWrappingRule wrappingRule) {
+		lineWrappingAtEnd = wrappingRule;
+	}
+
+	@Override
+	public boolean requiresSplitForLineWrapping() {
+		return lineWrappingAtStart == LineWrappingRule.REQUIRED || lineWrappingAtEnd == LineWrappingRule.REQUIRED;
+	}
+
+	@Override
 	public void accept(final IBoxVisitor visitor) {
 		visitor.visit(this);
 	}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/StaticText.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/StaticText.java
index e56eaf0..c0b1473 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/StaticText.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/StaticText.java
@@ -35,6 +35,8 @@
 	private String text;
 	private FontSpec fontSpec;
 	private Color color;
+	private LineWrappingRule lineWrappingAtStart;
+	private LineWrappingRule lineWrappingAtEnd;
 
 	private final CharSequenceSplitter splitter = new CharSequenceSplitter();
 
@@ -114,6 +116,29 @@
 		return graphics.stringWidth(text.substring(text.length() - whitespaceCount, text.length()));
 	}
 
+	@Override
+	public LineWrappingRule getLineWrappingAtStart() {
+		return lineWrappingAtStart;
+	}
+
+	public void setLineWrappingAtStart(final LineWrappingRule wrappingRule) {
+		lineWrappingAtStart = wrappingRule;
+	}
+
+	@Override
+	public LineWrappingRule getLineWrappingAtEnd() {
+		return lineWrappingAtEnd;
+	}
+
+	public void setLineWrappingAtEnd(final LineWrappingRule wrappingRule) {
+		lineWrappingAtEnd = wrappingRule;
+	}
+
+	@Override
+	public boolean requiresSplitForLineWrapping() {
+		return lineWrappingAtStart == LineWrappingRule.REQUIRED || lineWrappingAtEnd == LineWrappingRule.REQUIRED;
+	}
+
 	public String getText() {
 		return text;
 	}
@@ -199,6 +224,9 @@
 		if (!(other instanceof StaticText)) {
 			return false;
 		}
+		if (!lineSplittingRulesAllowJoining((StaticText) other)) {
+			return false;
+		}
 		if (!hasEqualFont((StaticText) other)) {
 			return false;
 		}
@@ -209,6 +237,16 @@
 		return true;
 	}
 
+	private boolean lineSplittingRulesAllowJoining(final StaticText other) {
+		if (lineWrappingAtEnd == LineWrappingRule.REQUIRED) {
+			return false;
+		}
+		if (other.lineWrappingAtStart == LineWrappingRule.REQUIRED) {
+			return false;
+		}
+		return true;
+	}
+
 	private boolean hasEqualFont(final StaticText other) {
 		if (fontSpec != null && !fontSpec.equals(other.fontSpec)) {
 			return false;
@@ -258,6 +296,8 @@
 		tail.layout(graphics);
 		removeTail(tail);
 
+		adjustSplittingRules(tail);
+
 		return tail;
 	}
 
@@ -280,4 +320,13 @@
 		width -= tail.width;
 	}
 
+	private void adjustSplittingRules(final StaticText tail) {
+		if (width <= 0) {
+			tail.setLineWrappingAtStart(lineWrappingAtStart);
+		}
+		if (tail.getWidth() > 0) {
+			tail.setLineWrappingAtEnd(lineWrappingAtEnd);
+			lineWrappingAtEnd = LineWrappingRule.ALLOWED;
+		}
+	}
 }
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/TextContent.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/TextContent.java
index dcaa007..277d2a7 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/TextContent.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/TextContent.java
@@ -35,6 +35,8 @@
 	private int width;
 	private int height;
 	private int baseline;
+	private LineWrappingRule lineWrappingAtStart;
+	private LineWrappingRule lineWrappingAtEnd;
 
 	private IContent content;
 	private IPosition startPosition;
@@ -131,6 +133,29 @@
 		return graphics.stringWidth(text.substring(text.length() - whitespaceCount, text.length()));
 	}
 
+	@Override
+	public LineWrappingRule getLineWrappingAtStart() {
+		return lineWrappingAtStart;
+	}
+
+	public void setLineWrappingAtStart(final LineWrappingRule wrappingRule) {
+		lineWrappingAtStart = wrappingRule;
+	}
+
+	@Override
+	public LineWrappingRule getLineWrappingAtEnd() {
+		return lineWrappingAtEnd;
+	}
+
+	public void setLineWrappingAtEnd(final LineWrappingRule wrappingRule) {
+		lineWrappingAtEnd = wrappingRule;
+	}
+
+	@Override
+	public boolean requiresSplitForLineWrapping() {
+		return lineWrappingAtStart == LineWrappingRule.REQUIRED || lineWrappingAtEnd == LineWrappingRule.REQUIRED;
+	}
+
 	private void invalidateLayout() {
 		layoutValid = false;
 	}
@@ -253,6 +278,9 @@
 		if (!isAdjacent((TextContent) other)) {
 			return false;
 		}
+		if (!lineSplittingRulesAllowJoining((TextContent) other)) {
+			return false;
+		}
 		if (!hasEqualFont((TextContent) other)) {
 			return false;
 		}
@@ -273,6 +301,16 @@
 		return true;
 	}
 
+	private boolean lineSplittingRulesAllowJoining(final TextContent other) {
+		if (lineWrappingAtEnd == LineWrappingRule.REQUIRED) {
+			return false;
+		}
+		if (other.lineWrappingAtStart == LineWrappingRule.REQUIRED) {
+			return false;
+		}
+		return true;
+	}
+
 	private boolean hasEqualFont(final TextContent other) {
 		if (fontSpec != null && !fontSpec.equals(other.fontSpec)) {
 			return false;
@@ -306,6 +344,8 @@
 		endPosition = otherText.endPosition;
 		width += otherText.width;
 
+		lineWrappingAtEnd = otherText.lineWrappingAtEnd;
+
 		return true;
 	}
 
@@ -328,6 +368,8 @@
 		tail.layout(graphics);
 		removeTail(tail);
 
+		adjustSplittingRules(tail);
+
 		return tail;
 	}
 
@@ -347,6 +389,16 @@
 		width -= tail.width;
 	}
 
+	private void adjustSplittingRules(final TextContent tail) {
+		if (width <= 0) {
+			tail.setLineWrappingAtStart(lineWrappingAtStart);
+		}
+		if (tail.getWidth() > 0) {
+			tail.setLineWrappingAtEnd(lineWrappingAtEnd);
+			lineWrappingAtEnd = LineWrappingRule.ALLOWED;
+		}
+	}
+
 	@Override
 	public int getStartOffset() {
 		return startPosition.getOffset();
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/CSSBasedBoxModelBuilder.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/CSSBasedBoxModelBuilder.java
index 5eb5db1..1de33b8 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/CSSBasedBoxModelBuilder.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/CSSBasedBoxModelBuilder.java
@@ -14,7 +14,6 @@
 import static org.eclipse.vex.core.internal.boxes.BoxFactory.nodeReference;
 import static org.eclipse.vex.core.internal.boxes.BoxFactory.nodeReferenceWithText;
 import static org.eclipse.vex.core.internal.boxes.BoxFactory.rootBox;
-import static org.eclipse.vex.core.internal.boxes.BoxFactory.square;
 import static org.eclipse.vex.core.internal.boxes.BoxFactory.verticalBlock;
 import static org.eclipse.vex.core.internal.visualization.CssBoxFactory.frame;
 import static org.eclipse.vex.core.internal.visualization.CssBoxFactory.paragraph;
@@ -31,12 +30,16 @@
 import org.eclipse.vex.core.internal.boxes.IParentBox;
 import org.eclipse.vex.core.internal.boxes.IStructuralBox;
 import org.eclipse.vex.core.internal.boxes.InlineContainer;
+import org.eclipse.vex.core.internal.boxes.LineWrappingRule;
 import org.eclipse.vex.core.internal.boxes.RootBox;
+import org.eclipse.vex.core.internal.boxes.TextContent;
 import org.eclipse.vex.core.internal.css.CSS;
 import org.eclipse.vex.core.internal.css.StyleSheet;
 import org.eclipse.vex.core.internal.css.Styles;
 import org.eclipse.vex.core.internal.dom.CollectingNodeTraversal;
 import org.eclipse.vex.core.provisional.dom.BaseNodeVisitorWithResult;
+import org.eclipse.vex.core.provisional.dom.ContentRange;
+import org.eclipse.vex.core.provisional.dom.IContent;
 import org.eclipse.vex.core.provisional.dom.IDocument;
 import org.eclipse.vex.core.provisional.dom.IDocumentFragment;
 import org.eclipse.vex.core.provisional.dom.IElement;
@@ -201,17 +204,20 @@
 
 			@Override
 			public IInlineBox visit(final IText text) {
+				final IContent content = text.getContent();
+				final ContentRange textRange = text.getRange();
 				if (!CSS.PRE.equals(styles.getWhiteSpace())) { // TODO use IWhitespacePolicy
-					return textContent(text.getContent(), text.getRange(), styles);
+					return textContent(content, textRange, styles);
 				}
 
 				final InlineContainer lineContainer = inlineContainer();
-				final MultilineText lines = text.getContent().getMultilineText(text.getRange());
+				final MultilineText lines = content.getMultilineText(textRange);
 				for (int i = 0; i < lines.size(); i += 1) {
-					lineContainer.appendChild(textContent(text.getContent(), lines.getRange(i), styles));
-					if (text.getContent().isLineBreak(lines.getRange(i).getEndOffset())) {
-						lineContainer.appendChild(square(10));
+					final TextContent textLine = textContent(content, lines.getRange(i), styles);
+					if (content.isLineBreak(lines.getRange(i).getEndOffset())) {
+						textLine.setLineWrappingAtEnd(LineWrappingRule.REQUIRED);
 					}
+					lineContainer.appendChild(textLine);
 				}
 				return lineContainer;
 			}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/CssBoxFactory.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/CssBoxFactory.java
index 14d4e6f..dab371d 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/CssBoxFactory.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/CssBoxFactory.java
@@ -15,6 +15,7 @@
 import org.eclipse.vex.core.internal.boxes.IInlineBox;
 import org.eclipse.vex.core.internal.boxes.IStructuralBox;
 import org.eclipse.vex.core.internal.boxes.InlineFrame;
+import org.eclipse.vex.core.internal.boxes.LineWrappingRule;
 import org.eclipse.vex.core.internal.boxes.Margin;
 import org.eclipse.vex.core.internal.boxes.Padding;
 import org.eclipse.vex.core.internal.boxes.Paragraph;
@@ -57,7 +58,6 @@
 			paragraph.appendChild(child);
 		}
 		paragraph.setTextAlign(textAlign(styles));
-		paragraph.setPreservingWhitespace(CSS.PRE.equals(styles.getWhiteSpace()));
 		return paragraph;
 	}
 
@@ -69,6 +69,12 @@
 		return textContent;
 	}
 
+	public static TextContent textContentWithLineBreak(final IContent content, final ContentRange range, final Styles styles) {
+		final TextContent textContent = textContent(content, range, styles);
+		textContent.setLineWrappingAtEnd(LineWrappingRule.REQUIRED);
+		return textContent;
+	}
+
 	public static StaticText staticText(final String text, final Styles styles) {
 		final StaticText staticText = new StaticText();
 		staticText.setText(text);
@@ -77,6 +83,12 @@
 		return staticText;
 	}
 
+	public static StaticText staticTextWithLineBreak(final String text, final Styles styles) {
+		final StaticText staticText = staticText(text, styles);
+		staticText.setLineWrappingAtEnd(LineWrappingRule.REQUIRED);
+		return staticText;
+	}
+
 	public static Margin margin(final Styles styles) {
 		return new Margin(styles.getMarginTop(), styles.getMarginLeft(), styles.getMarginBottom(), styles.getMarginRight());
 	}