Merge branch 'master' into feature/newBoxModel
diff --git a/org.eclipse.vex.core.tests/META-INF/MANIFEST.MF b/org.eclipse.vex.core.tests/META-INF/MANIFEST.MF
index d371a47..398e62f 100644
--- a/org.eclipse.vex.core.tests/META-INF/MANIFEST.MF
+++ b/org.eclipse.vex.core.tests/META-INF/MANIFEST.MF
@@ -20,12 +20,14 @@
  com.ibm.icu;bundle-version="50.1.1"
 Bundle-RequiredExecutionEnvironment: JavaSE-1.6
 Bundle-ActivationPolicy: lazy
-Export-Package: org.eclipse.vex.core.internal.core;x-friends:="org.eclipse.vex.ui.tests",
+Export-Package: org.eclipse.vex.core.internal.boxes;x-friends:="org.eclipse.vex.ui.tests",
+ org.eclipse.vex.core.internal.core;x-friends:="org.eclipse.vex.ui.tests",
  org.eclipse.vex.core.internal.css;x-friends:="org.eclipse.vex.ui.tests",
+ org.eclipse.vex.core.internal.cursor;x-friends:="org.eclipse.vex.ui.tests",
  org.eclipse.vex.core.internal.dom;x-friends:="org.eclipse.vex.ui.tests",
  org.eclipse.vex.core.internal.io;x-friends:="org.eclipse.vex.ui.tests",
  org.eclipse.vex.core.internal.layout;x-friends:="org.eclipse.vex.ui.tests",
- org.eclipse.vex.core.internal.layout.endtoend;x-internal:=true,
+ org.eclipse.vex.core.internal.layout.endtoend;x-friends:="org.eclipse.vex.ui.tests",
  org.eclipse.vex.core.internal.validator;x-friends:="org.eclipse.vex.ui.tests",
  org.eclipse.vex.core.internal.widget;x-friends:="org.eclipse.vex.ui.tests",
  org.eclipse.vex.core.internal.widget.swt;x-friends:="org.eclipse.vex.ui.tests",
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/FakeContentBox.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/FakeContentBox.java
new file mode 100644
index 0000000..013c725
--- /dev/null
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/FakeContentBox.java
@@ -0,0 +1,152 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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;
+
+import org.eclipse.vex.core.internal.core.Color;
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+import org.eclipse.vex.core.provisional.dom.ContentRange;
+import org.eclipse.vex.core.provisional.dom.IContent;
+
+/**
+ * @author Florian Thienel
+ */
+public class FakeContentBox extends BaseBox implements IContentBox {
+
+	private final int startOffset;
+	private final int endOffset;
+	private final Rectangle area;
+
+	public FakeContentBox(final int startOffset, final int endOffset, final Rectangle area) {
+		this.startOffset = startOffset;
+		this.endOffset = endOffset;
+		this.area = area;
+	}
+
+	@Override
+	public int getAbsoluteTop() {
+		return area.getY();
+	}
+
+	@Override
+	public int getAbsoluteLeft() {
+		return area.getX();
+	}
+
+	@Override
+	public int getTop() {
+		return area.getY();
+	}
+
+	@Override
+	public int getLeft() {
+		return area.getX();
+	}
+
+	@Override
+	public int getWidth() {
+		return area.getWidth();
+	}
+
+	@Override
+	public int getHeight() {
+		return area.getHeight();
+	}
+
+	@Override
+	public Rectangle getBounds() {
+		return area;
+	}
+
+	@Override
+	public void accept(final IBoxVisitor visitor) {
+		// ignore
+	}
+
+	@Override
+	public <T> T accept(final IBoxVisitorWithResult<T> visitor) {
+		return null;
+	}
+
+	@Override
+	public void layout(final Graphics graphics) {
+		// ignore
+	}
+
+	@Override
+	public boolean reconcileLayout(final Graphics graphics) {
+		return false;
+	}
+
+	@Override
+	public void paint(final Graphics graphics) {
+		// ignore
+	}
+
+	@Override
+	public IContent getContent() {
+		return null;
+	}
+
+	@Override
+	public int getStartOffset() {
+		return startOffset;
+	}
+
+	@Override
+	public int getEndOffset() {
+		return endOffset;
+	}
+
+	@Override
+	public ContentRange getRange() {
+		return new ContentRange(startOffset, endOffset);
+	}
+
+	@Override
+	public boolean isEmpty() {
+		return getEndOffset() - getStartOffset() <= 1;
+	}
+
+	public boolean isAtStart(final int offset) {
+		return getStartOffset() == offset;
+	}
+
+	public boolean isAtEnd(final int offset) {
+		return getEndOffset() == offset;
+	}
+
+	@Override
+	public Rectangle getPositionArea(final Graphics graphics, final int offset) {
+		return area;
+	}
+
+	@Override
+	public int getOffsetForCoordinates(final Graphics graphics, final int x, final int y) {
+		return startOffset;
+	}
+
+	@Override
+	public void setParent(final IBox parent) {
+		// ignore
+	}
+
+	@Override
+	public IBox getParent() {
+		return null;
+	}
+
+	@Override
+	public void highlight(final Graphics graphics, final Color foreground, final Color background) {
+		//ignore
+	}
+
+}
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/TestFrame.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/TestFrame.java
new file mode 100644
index 0000000..83dcf7e
--- /dev/null
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/TestFrame.java
@@ -0,0 +1,66 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author Florian Thienel
+ */
+public class TestFrame {
+
+	private StructuralFrame box;
+
+	@Before
+	public void setUp() throws Exception {
+		box = new StructuralFrame();
+	}
+
+	@Test
+	public void whenCreatedHasNoMargin() throws Exception {
+		assertEquals(Margin.NULL, box.getMargin());
+	}
+
+	@Test
+	public void marginIsMutable() throws Exception {
+		final Margin margin = new Margin(1, 2, 3, 4);
+		box.setMargin(margin);
+		assertEquals(margin, box.getMargin());
+	}
+
+	@Test
+	public void whenCreatedHasNoBorder() throws Exception {
+		assertEquals(Border.NULL, box.getBorder());
+	}
+
+	@Test
+	public void borderIsMutable() throws Exception {
+		final Border border = new Border(1, 2, 3, 4);
+		box.setBorder(border);
+		assertEquals(border, box.getBorder());
+	}
+
+	@Test
+	public void whenCreatedHasNoPadding() throws Exception {
+		assertEquals(Padding.NULL, box.getPadding());
+	}
+
+	@Test
+	public void PaddingIsMutable() throws Exception {
+		final Padding padding = new Padding(1, 2, 3, 4);
+		box.setPadding(padding);
+		assertEquals(padding, box.getPadding());
+	}
+
+}
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/TestInlineContainer.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/TestInlineContainer.java
new file mode 100644
index 0000000..8c16167
--- /dev/null
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/TestInlineContainer.java
@@ -0,0 +1,200 @@
+/*******************************************************************************
+ * 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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Iterator;
+
+import org.eclipse.vex.core.internal.core.Color;
+import org.eclipse.vex.core.internal.core.FontSpec;
+import org.eclipse.vex.core.internal.layout.FakeGraphics;
+import org.junit.Before;
+import org.junit.Test;
+
+public class TestInlineContainer {
+
+	private final static FontSpec FONT = new FontSpec("fontname", 0, 10.0f);
+	private FakeGraphics graphics;
+
+	@Before
+	public void setUp() throws Exception {
+		graphics = new FakeGraphics();
+	}
+
+	@Test
+	public void givenSeveralJoinableChildren_shouldJoinChildren() throws Exception {
+		final InlineContainer container = new InlineContainer();
+		container.appendChild(staticText("Lorem "));
+		container.appendChild(staticText("ipsum"));
+
+		assertEquals(1, count(container.getChildren()));
+	}
+
+	@Test
+	public void givenTwoInlineContainers_shouldIndicateJoiningIsPossible() throws Exception {
+		final InlineContainer container1 = new InlineContainer();
+		final InlineContainer container2 = new InlineContainer();
+
+		assertTrue(container1.canJoin(container2));
+	}
+
+	@Test
+	public void givenInlineContainer_whenCheckedForDifferentInlineBox_shouldIndicateJoiningIsNotPossible() throws Exception {
+		final InlineContainer container = new InlineContainer();
+		final StaticText staticText = staticText("Lorem ipsum");
+
+		assertFalse(container.canJoin(staticText));
+	}
+
+	@Test
+	public void givenTwoEmptyInlineContainers_shouldJoin() throws Exception {
+		final InlineContainer container1 = new InlineContainer();
+		final InlineContainer container2 = new InlineContainer();
+
+		assertTrue(container1.join(container2));
+	}
+
+	@Test
+	public void givenTwoNonEmptyInlineContainers_whenJoiningAndChildrenDoMatch_shouldJoinAdjacentChildren() throws Exception {
+		final InlineContainer container1 = new InlineContainer();
+		container1.appendChild(staticText("Lorem "));
+		final InlineContainer container2 = new InlineContainer();
+		container2.appendChild(staticText("ipsum"));
+
+		container1.join(container2);
+
+		assertEquals(1, count(container1.getChildren()));
+		final StaticText joinedText = (StaticText) container1.getChildren().iterator().next();
+		assertEquals("Lorem ipsum", joinedText.getText());
+	}
+
+	@Test
+	public void givenTwoInlineContainers_whenJoiningAndChildrenDoNotMatch_shouldJustAppendChildren() throws Exception {
+		final InlineContainer container1 = new InlineContainer();
+		container1.appendChild(staticText("Lorem "));
+		final InlineContainer container2 = new InlineContainer();
+		container2.appendChild(square(15));
+
+		container1.join(container2);
+
+		assertEquals(2, count(container1.getChildren()));
+	}
+
+	@Test
+	public void givenTwoInlineContainers_whenJoining_shouldSetParentOfNewChildren() throws Exception {
+		final InlineContainer container1 = new InlineContainer();
+		container1.appendChild(staticText("Lorem "));
+		final InlineContainer container2 = new InlineContainer();
+		container2.appendChild(square(15));
+
+		container1.join(container2);
+
+		for (final IInlineBox child : container1.getChildren()) {
+			assertEquals(container1, child.getParent());
+		}
+	}
+
+	@Test
+	public void givenTwoInlineContainers_whenJoining_shouldAdaptWidthHeightAndBaseline() throws Exception {
+		final InlineContainer container1 = new InlineContainer();
+		container1.appendChild(staticText("Lorem "));
+		container1.layout(graphics);
+		final int container1Width = container1.getWidth();
+		final int container1Descend = container1.getHeight() - container1.getBaseline();
+		final int container1Baseline = container1.getBaseline();
+		final InlineContainer container2 = new InlineContainer();
+		container2.appendChild(square(15));
+		container2.layout(graphics);
+		final int container2Width = container2.getWidth();
+		final int container2Descend = container2.getHeight() - container2.getBaseline();
+		final int container2Baseline = container2.getBaseline();
+
+		container1.join(container2);
+
+		assertEquals("width", container1Width + container2Width, container1.getWidth());
+		assertEquals("height", Math.max(container1Baseline, container2Baseline) + Math.max(container1Descend, container2Descend), container1.getHeight());
+		assertEquals("baseline", Math.max(container1Baseline, container2Baseline), container1.getBaseline());
+	}
+
+	@Test
+	public void givenTwoInlineContainers_whenJoining_shouldAdaptLayout() throws Exception {
+		final InlineContainer container1 = new InlineContainer();
+		container1.appendChild(staticText("Lorem "));
+		container1.layout(graphics);
+		final InlineContainer container2 = new InlineContainer();
+		container2.appendChild(square(15));
+		container2.layout(graphics);
+
+		container1.join(container2);
+
+		final Iterator<IInlineBox> children = container1.getChildren().iterator();
+		final IInlineBox child1 = children.next();
+		final IInlineBox child2 = children.next();
+
+		assertEquals("child2 left", child1.getLeft() + child1.getWidth(), child2.getLeft());
+		assertEquals("child1 top", container1.getBaseline() - child1.getBaseline(), child1.getTop());
+		assertEquals("child2 top", container1.getBaseline() - child2.getBaseline(), child2.getTop());
+	}
+
+	@Test
+	public void givenEmptyContainer_shouldIndicateSplittingIsNotPossible() throws Exception {
+		final InlineContainer container = new InlineContainer();
+
+		assertFalse(container.canSplit());
+	}
+
+	@Test
+	public void givenNonEmptyContainer_shouldIndicateSplittingIsPossible() throws Exception {
+		final InlineContainer container = new InlineContainer();
+		container.appendChild(staticText("Lorem ipsum"));
+
+		assertTrue(container.canSplit());
+	}
+
+	@Test
+	public void givenContainerWithOneSplittableChild_whenSplittingWithoutForce_shouldSplitAtWhitespace() throws Exception {
+		final InlineContainer container = new InlineContainer();
+		container.appendChild(staticText("Lorem ipsum"));
+		container.layout(graphics);
+
+		final InlineContainer tail = container.splitTail(graphics, 50, false);
+		final String tailText = ((StaticText) tail.getChildren().iterator().next()).getText();
+		assertEquals("ipsum", tailText);
+	}
+
+	private static int count(final Iterable<?> iterable) {
+		int count = 0;
+		final Iterator<?> iter = iterable.iterator();
+		while (iter.hasNext()) {
+			iter.next();
+			count += 1;
+		}
+		return count;
+	}
+
+	private static StaticText staticText(final String text) {
+		final StaticText staticText = new StaticText();
+		staticText.setText(text);
+		staticText.setFont(FONT);
+		return staticText;
+	}
+
+	private static Square square(final int size) {
+		final Square square = new Square();
+		square.setSize(size);
+		square.setColor(Color.BLACK);
+		return square;
+	}
+
+}
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/TestLine.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/TestLine.java
new file mode 100644
index 0000000..6eafa91
--- /dev/null
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/TestLine.java
@@ -0,0 +1,52 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+/**
+ * @author Florian Thienel
+ */
+public class TestLine {
+
+	@Test
+	public void whenAppendingChild_shouldAddUpChildrensWidth() throws Exception {
+		final Square square1 = new Square();
+		square1.setSize(10);
+		final Square square2 = new Square();
+		square2.setSize(13);
+		final Line line = new Line();
+
+		line.appendChild(square1);
+		assertEquals(10, line.getWidth());
+
+		line.appendChild(square2);
+		assertEquals(23, line.getWidth());
+	}
+
+	@Test
+	public void whenPrependingChild_shouldAddUpChildrensWidth() throws Exception {
+		final Square square1 = new Square();
+		square1.setSize(10);
+		final Square square2 = new Square();
+		square2.setSize(13);
+		final Line line = new Line();
+
+		line.prependChild(square1);
+		assertEquals(10, line.getWidth());
+
+		line.prependChild(square2);
+		assertEquals(23, line.getWidth());
+	}
+
+}
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
new file mode 100644
index 0000000..1b774f0
--- /dev/null
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/TestLineArrangement.java
@@ -0,0 +1,165 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.eclipse.vex.core.internal.core.Color;
+import org.eclipse.vex.core.internal.core.FontSpec;
+import org.eclipse.vex.core.internal.core.TextAlign;
+import org.eclipse.vex.core.internal.layout.FakeGraphics;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author Florian Thienel
+ */
+public class TestLineArrangement {
+	private final static FontSpec FONT = new FontSpec("fontname", 0, 10.0f);
+	private FakeGraphics graphics;
+	private List<IInlineBox> joinableBoxes;
+	private List<IInlineBox> unjoinableBoxes;
+	private LineArrangement lines;
+
+	@Before
+	public void setUp() throws Exception {
+		graphics = new FakeGraphics();
+		joinableBoxes = boxes(staticText("Lor"), staticText("em ipsum front "), staticText("Lorem ipsum back"));
+		unjoinableBoxes = boxes(staticText("Lorem ipsum front"), square(10), staticText("Lorem ipsum back"));
+		lines = new LineArrangement();
+	}
+
+	@Test
+	public void givenAllBoxesFitIntoOneLine_shouldArrangeBoxesInOneLine() throws Exception {
+		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);
+		assertEquals(1, joinableBoxes.size());
+	}
+
+	@Test
+	public void givenUnjoinableBoxes_whenBoxesFitIntoSameLane_shouldNotJoinBoxes() throws Exception {
+		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);
+
+		assertEquals(2, boxes.size());
+		assertEquals("Lor", ((StaticText) boxes.get(1)).getText());
+	}
+
+	@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);
+
+		for (final IInlineBox box : boxes) {
+			if (box.getWidth() == 0) {
+				fail("Splitting left over an empty box.");
+			}
+		}
+	}
+
+	@Test
+	public void givenInlineContainerFollowedBySingleSpace_whenSplittingWithinSpace_shouldKeepSpaceOnFirstLine() throws Exception {
+		final List<IInlineBox> boxes = boxes(staticText("Lorem "), inlineContainer(staticText("ipsum")), staticText(" "));
+		layout(boxes);
+		final int widthOfHeadBoxes = boxes.get(0).getWidth() + boxes.get(1).getWidth();
+
+		lines.arrangeBoxes(graphics, boxes.listIterator(), widthOfHeadBoxes + 1, TextAlign.LEFT);
+
+		assertEquals(1, lines.getLines().size());
+		assertEquals(boxes.get(2), lines.getLines().iterator().next().getLastChild());
+	}
+
+	@Test
+	public void givenSquareFollowedBySingleSpace_whenSplittingWithinSpace_shouldKeepSpaceOnFirstLine() throws Exception {
+		final List<IInlineBox> boxes = boxes(staticText("Lorem "), square(15), staticText(" "));
+		layout(boxes);
+		final int widthOfHeadBoxes = boxes.get(0).getWidth() + boxes.get(1).getWidth();
+
+		lines.arrangeBoxes(graphics, boxes.listIterator(), widthOfHeadBoxes + 1, TextAlign.LEFT);
+
+		assertEquals(1, lines.getLines().size());
+		assertEquals(boxes.get(2), lines.getLines().iterator().next().getLastChild());
+	}
+
+	@Test
+	public void givenInlineContainerFollowedByTextThatStartsWithSpace_whenSplittingWithinText_shouldSplitAfterSpace() throws Exception {
+		final List<IInlineBox> boxes = boxes(staticText("Lorem "), inlineContainer(staticText("ipsum")), staticText(" dolor"));
+		layout(boxes);
+		final int widthOfHeadBoxes = boxes.get(0).getWidth() + boxes.get(1).getWidth();
+
+		lines.arrangeBoxes(graphics, boxes.listIterator(), widthOfHeadBoxes + 10, TextAlign.LEFT);
+
+		assertEquals(2, lines.getLines().size());
+		assertEquals(" ", ((StaticText) lines.getLines().iterator().next().getLastChild()).getText());
+	}
+
+	@Test
+	public void givenInlineContainerFollowedByTextThatStartsWithSpace_whenSplittingAnywhereWithinSpaceAndText_shouldSplitAfterSpace() throws Exception {
+		for (int x = 1; x < graphics.stringWidth(" dolor"); x += 1) {
+			final List<IInlineBox> boxes = boxes(staticText("Lorem "), inlineContainer(staticText("ipsum")), staticText(" dolor"));
+			layout(boxes);
+			final int widthOfHeadBoxes = boxes.get(0).getWidth() + boxes.get(1).getWidth();
+
+			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());
+		}
+	}
+
+	private void layout(final List<IInlineBox> boxes) {
+		for (final IInlineBox box : boxes) {
+			box.layout(graphics);
+		}
+	}
+
+	private static List<IInlineBox> boxes(final IInlineBox... boxes) {
+		return new ArrayList<IInlineBox>(Arrays.asList(boxes));
+	}
+
+	private static InlineContainer inlineContainer(final IInlineBox... boxes) {
+		final InlineContainer container = new InlineContainer();
+		for (final IInlineBox box : boxes) {
+			container.appendChild(box);
+		}
+		return container;
+	}
+
+	private static StaticText staticText(final String text) {
+		final StaticText staticText = new StaticText();
+		staticText.setText(text);
+		staticText.setFont(FONT);
+		return staticText;
+	}
+
+	private static Square square(final int size) {
+		final Square square = new Square();
+		square.setSize(size);
+		square.setColor(Color.BLACK);
+		return square;
+	}
+}
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/TestRootBox.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/TestRootBox.java
new file mode 100644
index 0000000..b3d3b26
--- /dev/null
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/TestRootBox.java
@@ -0,0 +1,55 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author Florian Thienel
+ */
+public class TestRootBox {
+
+	private RootBox box;
+
+	@Before
+	public void setUp() throws Exception {
+		box = new RootBox();
+	}
+
+	@Test
+	public void whenCreated_hasNoWidth() throws Exception {
+		assertEquals(0, box.getWidth());
+	}
+
+	@Test
+	public void widthIsMutable() throws Exception {
+		box.setWidth(100);
+		assertEquals(100, box.getWidth());
+	}
+
+	@Test
+	public void whenCreatedHasNoChildren() throws Exception {
+		assertFalse(box.hasChildren());
+	}
+
+	@Test
+	public void canAppendChild() throws Exception {
+		final VerticalBlock child = new VerticalBlock();
+		box.appendChild(child);
+		assertTrue(box.hasChildren());
+	}
+
+}
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/TestStaticText.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/TestStaticText.java
new file mode 100644
index 0000000..0464538
--- /dev/null
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/TestStaticText.java
@@ -0,0 +1,270 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import org.eclipse.vex.core.internal.core.FontSpec;
+import org.eclipse.vex.core.internal.layout.FakeGraphics;
+import org.junit.Test;
+
+/**
+ * @author Florian Thienel
+ */
+public class TestStaticText {
+
+	@Test
+	public void givenTwoStaticTexts_whenFontIsEqual_shouldIndicatePossibleJoin() throws Exception {
+		final StaticText front = new StaticText();
+		front.setFont(new FontSpec("font", FontSpec.BOLD, 10));
+		final StaticText back = new StaticText();
+		back.setFont(new FontSpec("font", FontSpec.BOLD, 10));
+
+		assertTrue(front.canJoin(back));
+	}
+
+	@Test
+	public void givenTwoStaticTexts_whenFontIsDifferent_shouldIndicateImpossibleJoin() throws Exception {
+		final StaticText front = new StaticText();
+		front.setFont(new FontSpec("frontFont", FontSpec.BOLD, 10));
+		final StaticText back = new StaticText();
+		back.setFont(new FontSpec("backFont", FontSpec.BOLD, 10));
+
+		assertFalse(front.canJoin(back));
+	}
+
+	@Test
+	public void whenJoiningWithStaticText_shouldAppendBackTextToFrontText() throws Exception {
+		final StaticText front = new StaticText();
+		front.setText("front");
+		final StaticText back = new StaticText();
+		back.setText("back");
+
+		front.join(back);
+		assertEquals("frontback", front.getText());
+	}
+
+	@Test
+	public void whenJoiningWithStaticText_shouldAddWidth() throws Exception {
+		final FakeGraphics graphics = new FakeGraphics();
+		final StaticText front = new StaticText();
+		front.setText("front");
+		front.layout(graphics);
+		final StaticText back = new StaticText();
+		back.setText("back");
+		back.layout(graphics);
+
+		front.join(back);
+		assertEquals(54, front.getWidth());
+	}
+
+	@Test
+	public void whenJoiningWithStaticText_shouldIndicateSuccessfulJoin() throws Exception {
+		final StaticText front = new StaticText();
+		front.setText("front");
+		final StaticText back = new StaticText();
+		back.setText("back");
+
+		assertTrue(front.join(back));
+	}
+
+	@Test
+	public void givenJoiningTwoStaticTexts_whenJoinIsImpossible_shouldIndicateFailedJoin() throws Exception {
+		final StaticText front = new StaticText();
+		front.setFont(new FontSpec("frontFont", FontSpec.BOLD, 10));
+		final StaticText back = new StaticText();
+		back.setFont(new FontSpec("backFont", FontSpec.BOLD, 10));
+
+		assertFalse(front.join(back));
+	}
+
+	@Test
+	public void splitAtCharacterBoundary() throws Exception {
+		final FakeGraphics graphics = new FakeGraphics();
+		final StaticText text = new StaticText();
+		text.setText("1234567890");
+		text.layout(graphics);
+
+		final StaticText tail = (StaticText) text.splitTail(graphics, 6, true);
+
+		assertSplitEquals("1", "234567890", text, tail);
+		assertEquals(6, text.getWidth());
+		assertEquals(54, tail.getWidth());
+	}
+
+	@Test
+	public void whenSplitting_shouldAdaptWidth() throws Exception {
+		final FakeGraphics graphics = new FakeGraphics();
+		final StaticText text = new StaticText();
+		text.setText("1234567890");
+		text.layout(graphics);
+
+		final StaticText tail = (StaticText) text.splitTail(graphics, 11, true);
+
+		assertEquals(6, text.getWidth());
+		assertEquals(54, tail.getWidth());
+	}
+
+	@Test
+	public void whenSplitting_shouldApplyFontToTheTail() throws Exception {
+		final FakeGraphics graphics = new FakeGraphics();
+		final StaticText text = new StaticText();
+		text.setText("1234567890");
+		text.layout(graphics);
+
+		final StaticText tail = (StaticText) text.splitTail(graphics, 15, true);
+
+		assertSame(text.getFont(), tail.getFont());
+	}
+
+	@Test
+	public void whenSplittingLeftOfTheFirstCharacter_shouldMoveAllContentToTheTail() throws Exception {
+		final FakeGraphics graphics = new FakeGraphics();
+		final StaticText text = new StaticText();
+		text.setText("1234567890");
+		text.layout(graphics);
+
+		final StaticText tail = (StaticText) text.splitTail(graphics, 5, true);
+
+		assertSplitEquals("", "1234567890", text, tail);
+	}
+
+	@Test
+	public void whenSplittingBeforeNextCharacterBoundary_shouldMoveNextCharacterToTheTail() throws Exception {
+		final FakeGraphics graphics = new FakeGraphics();
+		final StaticText text = new StaticText();
+		text.setText("1234567890");
+		text.layout(graphics);
+
+		final StaticText tail = (StaticText) text.splitTail(graphics, 11, true);
+
+		assertSplitEquals("1", "234567890", text, tail);
+	}
+
+	@Test
+	public void whenSplittingWayBehindTheBox_shouldReturnEmptyTail() throws Exception {
+		final FakeGraphics graphics = new FakeGraphics();
+		final StaticText text = new StaticText();
+		text.setText("1234567890");
+		text.layout(graphics);
+
+		final StaticText tail = (StaticText) text.splitTail(graphics, 100, true);
+
+		assertSplitEquals("1234567890", "", text, tail);
+	}
+
+	@Test
+	public void givenTextContainsWhitespace_whenSplittingAfterWhitespace_shouldSplitRightAfterWhitespace() throws Exception {
+		final FakeGraphics graphics = new FakeGraphics();
+		final StaticText text = new StaticText();
+		text.setText("1234 567890");
+		text.layout(graphics);
+
+		final StaticText tail = (StaticText) text.splitTail(graphics, 34, false);
+
+		assertSplitEquals("1234 ", "567890", text, tail);
+	}
+
+	@Test
+	public void givenTextContainsWhitespace_whenSplittingBeforeFirstWhitespace_shouldMoveAllContentToTheTail() throws Exception {
+		final FakeGraphics graphics = new FakeGraphics();
+		final StaticText text = new StaticText();
+		text.setText("1234 567890");
+		text.layout(graphics);
+
+		final StaticText tail = (StaticText) text.splitTail(graphics, 15, false);
+
+		assertSplitEquals("", "1234 567890", text, tail);
+	}
+
+	@Test
+	public void givenTextContainsWhitespace_whenSplittingWithinWhitespace_shouldSplitBeforeWhitespace() throws Exception {
+		final FakeGraphics graphics = new FakeGraphics();
+		final StaticText text = new StaticText();
+		text.setText("12 34 567890");
+		text.layout(graphics);
+
+		final StaticText tail = (StaticText) text.splitTail(graphics, 34, false);
+
+		assertSplitEquals("12 ", "34 567890", text, tail);
+	}
+
+	@Test
+	public void givenTextContainsWhitespace_whenSplittingRightAfterWhitespace_shouldSplitRightAfterWhitespace() throws Exception {
+		final FakeGraphics graphics = new FakeGraphics();
+		final StaticText text = new StaticText();
+		text.setText("1234 567890");
+		text.layout(graphics);
+
+		final StaticText tail = (StaticText) text.splitTail(graphics, 30, false);
+
+		assertSplitEquals("1234 ", "567890", text, tail);
+	}
+
+	@Test
+	public void givenTextWithWhitespaceAtEnd_shouldProvideWidthOfWhitespace() throws Exception {
+		final FakeGraphics graphics = new FakeGraphics();
+		final StaticText text = new StaticText();
+		text.setText("abc ");
+		text.layout(graphics);
+
+		assertEquals(graphics.stringWidth(" "), text.getInvisibleGapAtEnd(graphics));
+	}
+
+	@Test
+	public void givenTextWithoutWhitespaceAtEnd_shouldProvideZeroWidthOfWhitespace() throws Exception {
+		final FakeGraphics graphics = new FakeGraphics();
+		final StaticText text = new StaticText();
+		text.setText("abc");
+		text.layout(graphics);
+
+		assertEquals(0, text.getInvisibleGapAtEnd(graphics));
+	}
+
+	@Test
+	public void givenTextWithWhitespaceAtStart_shouldProvideWidthOfWhitespace() throws Exception {
+		final FakeGraphics graphics = new FakeGraphics();
+		final StaticText text = new StaticText();
+		text.setText(" abc");
+		text.layout(graphics);
+
+		assertEquals(graphics.stringWidth(" "), text.getInvisibleGapAtStart(graphics));
+	}
+
+	@Test
+	public void givenTextWithoutWhitespaceAtStart_shouldProvideZeroWidthOfWhitespace() throws Exception {
+		final FakeGraphics graphics = new FakeGraphics();
+		final StaticText text = new StaticText();
+		text.setText("abc");
+		text.layout(graphics);
+
+		assertEquals(0, text.getInvisibleGapAtStart(graphics));
+	}
+
+	@Test
+	public void givenTextWithOnlyWhitespace_shouldProvideSameWidthOfWhitespaceForLeftAndRight() throws Exception {
+		final FakeGraphics graphics = new FakeGraphics();
+		final StaticText text = new StaticText();
+		text.setText("   ");
+		text.layout(graphics);
+
+		assertEquals("left", graphics.stringWidth("   "), text.getInvisibleGapAtStart(graphics));
+		assertEquals("right", graphics.stringWidth("   "), text.getInvisibleGapAtEnd(graphics));
+	}
+
+	private static void assertSplitEquals(final String head, final String tail, final StaticText headBox, final StaticText tailBox) {
+		assertEquals(head, headBox.getText());
+		assertEquals(tail, tailBox.getText());
+	}
+}
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/TestVerticalBlock.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/TestVerticalBlock.java
new file mode 100644
index 0000000..40c8cb4
--- /dev/null
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/TestVerticalBlock.java
@@ -0,0 +1,68 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author Florian Thienel
+ */
+public class TestVerticalBlock {
+
+	private VerticalBlock box;
+
+	@Before
+	public void setUp() throws Exception {
+		box = new VerticalBlock();
+	}
+
+	@Test
+	public void whenCreatedIsAtOrigin() throws Exception {
+		assertEquals(0, box.getTop());
+		assertEquals(0, box.getLeft());
+	}
+
+	@Test
+	public void positionIsMutable() throws Exception {
+		box.setPosition(12, 34);
+		assertEquals(12, box.getTop());
+		assertEquals(34, box.getLeft());
+	}
+
+	@Test
+	public void whenCreatedHasNoWidth() throws Exception {
+		assertEquals(0, box.getWidth());
+	}
+
+	@Test
+	public void widthIsMutable() throws Exception {
+		box.setWidth(1234);
+		assertEquals(1234, box.getWidth());
+	}
+
+	@Test
+	public void whenCreatedHasNoChildren() throws Exception {
+		assertFalse(box.hasChildren());
+	}
+
+	@Test
+	public void canAppendChild() throws Exception {
+		final VerticalBlock child = new VerticalBlock();
+		box.appendChild(child);
+		assertTrue(box.hasChildren());
+	}
+
+}
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/TestWithTracing.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/TestWithTracing.java
new file mode 100644
index 0000000..02b06a9
--- /dev/null
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/TestWithTracing.java
@@ -0,0 +1,98 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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;
+
+import static org.eclipse.vex.core.internal.boxes.BoxFactory.frame;
+import static org.eclipse.vex.core.internal.boxes.BoxFactory.horizontalBar;
+import static org.eclipse.vex.core.internal.boxes.BoxFactory.verticalBlock;
+import static org.junit.Assert.assertEquals;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
+
+import org.eclipse.vex.core.internal.core.Color;
+import org.eclipse.vex.core.internal.core.DisplayDevice;
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.layout.endtoend.TracingHostComponent;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+
+/**
+ * @author Florian Thienel
+ */
+public class TestWithTracing {
+
+	private RootBox rootBox;
+
+	@Rule
+	public TestName name = new TestName();
+
+	@Before
+	public void setUp() throws Exception {
+		rootBox = new RootBox();
+	}
+
+	@Test
+	public void verticalBlockHorizontalBar() throws Exception {
+		final StructuralFrame frame = frame(verticalBlock(horizontalBar(10, Color.BLACK)), new Margin(10, 20, 30, 40), new Border(10), new Padding(15, 25, 35, 45), null);
+
+		rootBox.appendChild(frame);
+		rootBox.setWidth(300);
+
+		final String expected = normalizeLineSeparators(readExpectedTrace());
+		final String actual = normalizeLineSeparators(traceRendering(rootBox));
+
+		assertEquals(expected, actual);
+	}
+
+	private static String traceRendering(final RootBox rootBox) {
+		final ByteArrayOutputStream traceBuffer = new ByteArrayOutputStream();
+		final PrintStream printStream = new PrintStream(traceBuffer);
+
+		DisplayDevice.setCurrent(DisplayDevice._72DPI);
+		final TracingHostComponent hostComponent = new TracingHostComponent(printStream);
+		final Graphics graphics = hostComponent.createDefaultGraphics();
+		rootBox.layout(graphics);
+		rootBox.paint(graphics);
+		return new String(traceBuffer.toByteArray());
+	}
+
+	private static String normalizeLineSeparators(final String s) {
+		return s.replaceAll("[\n\r]+", "\n");
+	}
+
+	private static String readAndCloseStream(final InputStream in) throws IOException {
+		try {
+			final ByteArrayOutputStream content = new ByteArrayOutputStream();
+			final byte[] readBuffer = new byte[1024];
+			int readCount;
+			while ((readCount = in.read(readBuffer)) > 0) {
+				content.write(readBuffer, 0, readCount);
+			}
+			return new String(content.toByteArray());
+		} finally {
+			in.close();
+		}
+	}
+
+	private String readExpectedTrace() throws IOException {
+		return readAndCloseStream(getClass().getResourceAsStream(outputName()));
+	}
+
+	private String outputName() {
+		return name.getMethodName() + "-output.txt";
+	}
+
+}
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/verticalBlockHorizontalBar-output.txt b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/verticalBlockHorizontalBar-output.txt
new file mode 100644
index 0000000..15b0e3e
--- /dev/null
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/verticalBlockHorizontalBar-output.txt
@@ -0,0 +1,30 @@
+HostComponent.createDefaultGraphics()
+Graphics.moveOrigin(0, 0)
+Graphics.setLineStyle(SOLID)
+Graphics.setLineWidth(10)
+Graphics.createColor(Color[r=0,g=0,b=0])
+Graphics.setColor(TracingColorResource [color=Color[r=0,g=0,b=0], id=-1])
+Graphics.drawLine(20, 15, 260, 15)
+Graphics.setLineStyle(SOLID)
+Graphics.setLineWidth(10)
+Graphics.createColor(Color[r=0,g=0,b=0])
+Graphics.setColor(TracingColorResource [color=Color[r=0,g=0,b=0], id=-1])
+Graphics.drawLine(25, 10, 25, 90)
+Graphics.setLineStyle(SOLID)
+Graphics.setLineWidth(10)
+Graphics.createColor(Color[r=0,g=0,b=0])
+Graphics.setColor(TracingColorResource [color=Color[r=0,g=0,b=0], id=-1])
+Graphics.drawLine(20, 85, 260, 85)
+Graphics.setLineStyle(SOLID)
+Graphics.setLineWidth(10)
+Graphics.createColor(Color[r=0,g=0,b=0])
+Graphics.setColor(TracingColorResource [color=Color[r=0,g=0,b=0], id=-1])
+Graphics.drawLine(255, 10, 255, 90)
+Graphics.moveOrigin(55, 35)
+Graphics.moveOrigin(0, 0)
+Graphics.createColor(Color[r=0,g=0,b=0])
+Graphics.setColor(TracingColorResource [color=Color[r=0,g=0,b=0], id=-1])
+Graphics.fillRect(0, 0, 150, 10)
+Graphics.moveOrigin(0, 0)
+Graphics.moveOrigin(-55, -35)
+Graphics.moveOrigin(0, 0)
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/core/TextUtilsTest.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/core/TextUtilsTest.java
new file mode 100644
index 0000000..ddca0d1
--- /dev/null
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/core/TextUtilsTest.java
@@ -0,0 +1,50 @@
+/*******************************************************************************
+ * 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.core;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.Arrays;
+
+import org.junit.Test;
+
+public class TextUtilsTest {
+
+	@Test
+	public void provideLinesDelimitedWithLF() throws Exception {
+		assertEquals(Arrays.asList("line 1", "line 2", "line 3"), Arrays.asList(TextUtils.lines("line 1\nline 2\nline 3")));
+	}
+
+	@Test
+	public void provideLinesDelimitedWithCR() throws Exception {
+		assertEquals(Arrays.asList("line 1", "line 2", "line 3"), Arrays.asList(TextUtils.lines("line 1\rline 2\rline 3")));
+	}
+
+	@Test
+	public void provideLinesDelimitedWithCRLF() throws Exception {
+		assertEquals(Arrays.asList("line 1", "line 2", "line 3"), Arrays.asList(TextUtils.lines("line 1\r\nline 2\r\nline 3")));
+	}
+
+	@Test
+	public void provideLinesDelimitedWithDifferentLineSeparators() throws Exception {
+		assertEquals(Arrays.asList("line 1", "line 2", "line 3", "line 4", "line 5"), Arrays.asList(TextUtils.lines("line 1\rline 2\nline 3\r\nline 4\rline 5")));
+	}
+
+	@Test
+	public void provideEmptyLines() throws Exception {
+		assertEquals(Arrays.asList("", "line 1", "", "line 2", "line 3", ""), Arrays.asList(TextUtils.lines("\nline 1\n\nline 2\nline 3\n")));
+	}
+
+	@Test
+	public void provideSingleLine() throws Exception {
+		assertEquals(Arrays.asList("line 1"), Arrays.asList(TextUtils.lines("line 1")));
+	}
+}
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/css/BatikBehaviorTest.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/css/BatikBehaviorTest.java
index e3542da..4737763 100644
--- a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/css/BatikBehaviorTest.java
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/css/BatikBehaviorTest.java
@@ -50,7 +50,7 @@
 		final Element element = new Element("plan");
 		final IElement before = styleSheet.getPseudoElementBefore(element);
 		final Styles beforeStyles = styleSheet.getStyles(before);
-		assertEquals("test", beforeStyles.getContent(element).get(0));
+		assertEquals("test", beforeStyles.getTextualContent());
 		assertEquals(123.0f, beforeStyles.getFontSize(), 0.0f);
 	}
 
@@ -63,7 +63,7 @@
 		assertEquals(123.0f, styles.getFontSize(), 0.0f);
 		final IElement before = styleSheet.getPseudoElementBefore(element);
 		final Styles beforeStyles = styleSheet.getStyles(before);
-		assertEquals("test", beforeStyles.getContent(element).get(0));
+		assertEquals("test", beforeStyles.getTextualContent());
 		assertEquals(123.0f, beforeStyles.getFontSize(), 0.0f);
 	}
 
@@ -76,9 +76,8 @@
 		final Element nochild = new Element("child");
 		child.setParent(element);
 		final Styles styles = styleSheet.getStyles(child);
-		assertEquals(1, styles.getContent(element).size());
-		assertEquals("child", styles.getContent(element).get(0));
+		assertEquals("child", styles.getTextualContent());
 		final Styles nochildStyles = styleSheet.getStyles(nochild);
-		assertEquals("nochild", nochildStyles.getContent(element).get(0));
+		assertEquals("nochild", nochildStyles.getTextualContent());
 	}
 }
\ No newline at end of file
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/css/BulletStyleTest.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/css/BulletStyleTest.java
new file mode 100644
index 0000000..7ec2c65
--- /dev/null
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/css/BulletStyleTest.java
@@ -0,0 +1,66 @@
+/*******************************************************************************
+ * Copyright (c) 2016 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.css;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+public class BulletStyleTest {
+
+	@Test
+	public void decimalBulletText() throws Exception {
+		assertEquals("1.", BulletStyle.toDecimal(0));
+		assertEquals("9.", BulletStyle.toDecimal(8));
+		assertEquals("10.", BulletStyle.toDecimal(9));
+	}
+
+	@Test
+	public void decimalWithLeadingZerosBulletText() throws Exception {
+		assertEquals("1 digit", "1.", BulletStyle.toDecimalWithLeadingZeroes(0, 1));
+		assertEquals("2 digits", "01.", BulletStyle.toDecimalWithLeadingZeroes(0, 12));
+		assertEquals("3 digits", "001.", BulletStyle.toDecimalWithLeadingZeroes(0, 123));
+		assertEquals("4 digits", "0001.", BulletStyle.toDecimalWithLeadingZeroes(0, 1234));
+	}
+
+	@Test
+	public void lowerRomanBulletText() throws Exception {
+		assertEquals("i.", BulletStyle.toLowerRoman(0));
+		assertEquals("ii.", BulletStyle.toLowerRoman(1));
+		assertEquals("ix.", BulletStyle.toLowerRoman(8));
+		assertEquals("x.", BulletStyle.toLowerRoman(9));
+	}
+
+	@Test
+	public void upperRomanBulletText() throws Exception {
+		assertEquals("I.", BulletStyle.toUpperRoman(0));
+		assertEquals("II.", BulletStyle.toUpperRoman(1));
+		assertEquals("IX.", BulletStyle.toUpperRoman(8));
+		assertEquals("X.", BulletStyle.toUpperRoman(9));
+	}
+
+	@Test
+	public void lowerLatinBulletText() throws Exception {
+		assertEquals("a.", BulletStyle.toLowerLatin(0));
+		assertEquals("b.", BulletStyle.toLowerLatin(1));
+		assertEquals("y.", BulletStyle.toLowerLatin(24));
+		assertEquals("z.", BulletStyle.toLowerLatin(25));
+		assertEquals("aa.", BulletStyle.toLowerLatin(26));
+		assertEquals("ab.", BulletStyle.toLowerLatin(27));
+		assertEquals("ba.", BulletStyle.toLowerLatin(52));
+		assertEquals("zz.", BulletStyle.toLowerLatin(701));
+		assertEquals("aaa.", BulletStyle.toLowerLatin(702));
+		assertEquals("azz.", BulletStyle.toLowerLatin(1377));
+		assertEquals("baa.", BulletStyle.toLowerLatin(1378));
+		assertEquals("zzz.", BulletStyle.toLowerLatin(18277));
+		assertEquals("aaaa.", BulletStyle.toLowerLatin(18278));
+	}
+}
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/css/CssTest.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/css/CssTest.java
index 3389232..00f97b0 100644
--- a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/css/CssTest.java
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/css/CssTest.java
@@ -579,9 +579,9 @@
 
 		final Styles styles = ss.getStyles(element);
 
-		assertEquals("Before", styles.getContent(element).get(0));
+		assertEquals("Before", styles.getTextualContent());
 		element.setAttribute("attribute", "After");
-		assertEquals("After", styles.getContent(element).get(0));
+		assertEquals("After", styles.getTextualContent());
 	}
 
 	@Test
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/css/MockLU.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/css/MockLU.java
index 3fe3306..477ed0e 100644
--- a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/css/MockLU.java
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/css/MockLU.java
@@ -21,32 +21,57 @@
 		lexicalUnitType = type;
 	}
 
-	public static LexicalUnit INHERIT = new MockLU(LexicalUnit.SAC_INHERIT);
+	public static final MockLU INHERIT = new MockLU(LexicalUnit.SAC_INHERIT);
 
-	public static LexicalUnit createFloat(final short units, final float value) {
+	public static MockLU createFloat(final short units, final float value) {
 		final MockLU lu = new MockLU(units);
 		lu.setFloatValue(value);
 		return lu;
 	}
 
-	public static LexicalUnit createIdent(final String s) {
+	public static MockLU createIdent(final String s) {
 		final MockLU lu = new MockLU(LexicalUnit.SAC_IDENT);
 		lu.setStringValue(s);
 		return lu;
 	}
 
-	public static LexicalUnit createString(final String s) {
+	public static MockLU createString(final String s) {
 		final MockLU lu = new MockLU(LexicalUnit.SAC_STRING_VALUE);
 		lu.setStringValue(s);
 		return lu;
 	}
 
-	public static LexicalUnit createAttr(final String attributeName) {
+	public static MockLU createAttr(final String attributeName) {
 		final MockLU result = new MockLU(LexicalUnit.SAC_ATTR);
 		result.setStringValue(attributeName);
 		return result;
 	}
 
+	public static MockLU createUri(final String uri) {
+		final MockLU result = new MockLU(LexicalUnit.SAC_URI);
+		result.setStringValue(uri);
+		return result;
+	}
+
+	public static MockLU createImage(final MockLU... parameters) {
+		final MockLU result = new MockLU(LexicalUnit.SAC_FUNCTION);
+		result.setFunctionName(CSS.IMAGE_FUNCTION);
+		MockLU firstParameter = null;
+		MockLU lastParameter = null;
+		for (final MockLU parameter : parameters) {
+			if (firstParameter == null) {
+				firstParameter = parameter;
+			}
+			if (lastParameter != null) {
+				lastParameter.setNextLexicalUnit(parameter);
+				parameter.setPreviousLexicalUnit(lastParameter);
+			}
+			lastParameter = parameter;
+		}
+		result.setParameters(firstParameter);
+		return result;
+	}
+
 	@Override
 	public String getDimensionUnitText() {
 		return dimensionUnitText;
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/css/PropertyTest.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/css/PropertyTest.java
index a959da0..48dd2fb 100644
--- a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/css/PropertyTest.java
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/css/PropertyTest.java
@@ -16,11 +16,13 @@
 
 import java.io.StringReader;
 import java.util.Iterator;
+import java.util.List;
 
 import org.eclipse.vex.core.internal.core.DisplayDevice;
 import org.eclipse.vex.core.internal.io.DocumentReader;
 import org.eclipse.vex.core.provisional.dom.IDocument;
 import org.eclipse.vex.core.provisional.dom.IElement;
+import org.eclipse.vex.core.provisional.dom.IProcessingInstruction;
 import org.junit.Test;
 import org.w3c.css.sac.InputSource;
 import org.w3c.css.sac.LexicalUnit;
@@ -143,13 +145,65 @@
 		final IElement parent = document.getRootElement().childElements().first();
 		final IElement child = parent.childElements().first();
 		final OutlineContentProperty property = new OutlineContentProperty();
-		final MockLU lu = (MockLU) MockLU.createIdent("child");
+		final MockLU lu = MockLU.createIdent("child");
 		lu.setNextLexicalUnit(MockLU.createIdent("none"));
 
 		assertEquals(child, property.calculate(lu, styles, null, parent));
 		assertEquals(null, property.calculate(lu, styles, null, child));
 	}
 
+	@Test
+	public void contentProperty_textualContent() throws Exception {
+		final ContentProperty property = new ContentProperty();
+		final LexicalUnit lu = MockLU.createString("textual content");
+		final List<IPropertyContent> propertyContent = property.calculate(lu, null, null, null);
+		assertEquals(1, propertyContent.size());
+		assertEquals("textual content", propertyContent.get(0).toString());
+	}
+
+	@Test
+	public void contentProperty_attributeDependendContent() throws Exception {
+		final IDocument document = new DocumentReader().read("<root><elem textAttr=\"textual content from attribute\"/></root>");
+		final IElement element = document.getRootElement().childElements().first();
+		final LexicalUnit lu = MockLU.createAttr("textAttr");
+		final List<IPropertyContent> propertyContent = new ContentProperty().calculate(lu, null, null, element);
+		assertEquals(1, propertyContent.size());
+		final IPropertyContent firstContent = propertyContent.get(0);
+		assertEquals("textual content from attribute", firstContent.toString());
+
+		element.setAttribute("textAttr", "changed textual content");
+		assertEquals("changed textual content", firstContent.toString());
+	}
+
+	@Test
+	public void contentProperty_processingInstructionTarget() throws Exception {
+		final IDocument document = new DocumentReader().read("<root><?piTarget?></root>");
+		final IProcessingInstruction pi = (IProcessingInstruction) document.getRootElement().children().first();
+		final LexicalUnit lu = MockLU.createAttr("target");
+		final List<IPropertyContent> propertyContent = new ContentProperty().calculate(lu, null, null, pi);
+		assertEquals(1, propertyContent.size());
+		final IPropertyContent firstContent = propertyContent.get(0);
+		assertEquals("piTarget", firstContent.toString());
+
+		pi.setTarget("anotherTarget");
+		assertEquals("anotherTarget", firstContent.toString());
+	}
+
+	@Test
+	public void contentProperty_image_attributeValue() throws Exception {
+		final IDocument document = new DocumentReader().read("<root><image src=\"image.jpg\"/></root>");
+		final IElement imageElement = document.getRootElement().childElements().first();
+		imageElement.setBaseURI("https://www.eclipse.org");
+		final LexicalUnit lu = MockLU.createImage(MockLU.createAttr("src"));
+		final List<IPropertyContent> propertyContent = new ContentProperty().calculate(lu, null, null, imageElement);
+		assertEquals(1, propertyContent.size());
+		final IPropertyContent firstContent = propertyContent.get(0);
+		assertEquals("https://www.eclipse.org/image.jpg", ((ImageContent) firstContent).getResolvedImageURL().toString());
+
+		imageElement.setAttribute("src", "anotherImage.jpg");
+		assertEquals("https://www.eclipse.org/anotherImage.jpg", ((ImageContent) firstContent).getResolvedImageURL().toString());
+	}
+
 	private static class DummyDisplayDevice extends DisplayDevice {
 		public DummyDisplayDevice(final int horizontalPPI, final int verticalPPI) {
 			this.horizontalPPI = horizontalPPI;
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/cursor/FakeSelector.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/cursor/FakeSelector.java
new file mode 100644
index 0000000..cad0b8e
--- /dev/null
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/cursor/FakeSelector.java
@@ -0,0 +1,57 @@
+/*******************************************************************************
+ * 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.cursor;
+
+import org.eclipse.vex.core.provisional.dom.ContentRange;
+
+/**
+ * @author Florian Thienel
+ */
+public class FakeSelector implements IContentSelector {
+
+	@Override
+	public void setMark(final int offset) {
+	}
+
+	@Override
+	public void moveEndTo(final int offset) {
+	}
+
+	@Override
+	public void setEndAbsoluteTo(final int offset) {
+	}
+
+	@Override
+	public boolean isActive() {
+		return false;
+	}
+
+	@Override
+	public int getStartOffset() {
+		return 0;
+	}
+
+	@Override
+	public int getEndOffset() {
+		return 0;
+	}
+
+	@Override
+	public ContentRange getRange() {
+		return ContentRange.NULL;
+	}
+
+	@Override
+	public int getCaretOffset() {
+		return 0;
+	}
+
+}
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/cursor/TestCursorPosition.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/cursor/TestCursorPosition.java
new file mode 100644
index 0000000..8661bc9
--- /dev/null
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/cursor/TestCursorPosition.java
@@ -0,0 +1,540 @@
+/*******************************************************************************
+ * 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
+ *      Carsten Hiesserich - moveToNextWord, moveToPreviousWord
+ *******************************************************************************/
+package org.eclipse.vex.core.internal.cursor;
+
+import static org.eclipse.vex.core.internal.cursor.CursorMoves.down;
+import static org.eclipse.vex.core.internal.cursor.CursorMoves.left;
+import static org.eclipse.vex.core.internal.cursor.CursorMoves.right;
+import static org.eclipse.vex.core.internal.cursor.CursorMoves.toAbsoluteCoordinates;
+import static org.eclipse.vex.core.internal.cursor.CursorMoves.toNextWord;
+import static org.eclipse.vex.core.internal.cursor.CursorMoves.toOffset;
+import static org.eclipse.vex.core.internal.cursor.CursorMoves.toPreviousWord;
+import static org.eclipse.vex.core.internal.cursor.CursorMoves.up;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import org.eclipse.vex.core.internal.boxes.DepthFirstBoxTraversal;
+import org.eclipse.vex.core.internal.boxes.IContentBox;
+import org.eclipse.vex.core.internal.boxes.InlineNodeReference;
+import org.eclipse.vex.core.internal.boxes.NodeEndOffsetPlaceholder;
+import org.eclipse.vex.core.internal.boxes.RootBox;
+import org.eclipse.vex.core.internal.boxes.StructuralNodeReference;
+import org.eclipse.vex.core.internal.boxes.TextContent;
+import org.eclipse.vex.core.internal.io.UniversalTestDocument;
+import org.eclipse.vex.core.internal.layout.FakeGraphics;
+import org.eclipse.vex.core.internal.visualization.DocumentRootVisualization;
+import org.eclipse.vex.core.internal.visualization.ParagraphVisualization;
+import org.eclipse.vex.core.internal.visualization.StructureElementVisualization;
+import org.eclipse.vex.core.internal.visualization.TextVisualization;
+import org.eclipse.vex.core.internal.visualization.VisualizationChain;
+import org.eclipse.vex.core.internal.widget.FakeViewPort;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author Florian Thienel
+ */
+public class TestCursorPosition {
+
+	private RootBox rootBox;
+	private Cursor cursor;
+	private FakeGraphics graphics;
+	private UniversalTestDocument document;
+
+	@Before
+	public void setUp() throws Exception {
+		graphics = new FakeGraphics();
+		document = new UniversalTestDocument(2);
+		cursor = new Cursor(new FakeSelector(), new FakeViewPort());
+
+		visualizeDocument();
+	}
+
+	private void visualizeDocument() {
+		rootBox = buildVisualizationChain().visualizeRoot(document.getDocument());
+		cursor.setRootBox(rootBox);
+		rootBox.setWidth(200);
+		rootBox.layout(graphics);
+	}
+
+	@Test
+	public void canMoveCursorOneCharacterLeft() throws Exception {
+		final int position = document.getOffsetWithinText(0);
+		cursorAt(position);
+		cursor.move(left());
+		assertCursorAt(position - 1);
+	}
+
+	@Test
+	public void whenAtFirstPosition_cannotMoveCursorOneCharacterLeft() throws Exception {
+		cursorAt(0);
+		cursor.move(left());
+		assertCursorAt(0);
+	}
+
+	@Test
+	public void canMoveCursorOneCharacterRight() throws Exception {
+		cursorAt(5);
+		cursor.move(right());
+		assertCursorAt(6);
+	}
+
+	@Test
+	public void whenAtLastOffset_cannotMoveCursorOneCharacterRight() throws Exception {
+		cursorAt(lastOffset());
+		cursor.move(right());
+		assertCursorAt(lastOffset());
+	}
+
+	@Test
+	public void canMoveCursorOneLineUp() throws Exception {
+		cursorAt(35);
+		moveCursor(up());
+		assertCursorAt(5);
+	}
+
+	@Test
+	public void whenAtFirstPosition_cannotMoveCursorOneLineUp() throws Exception {
+		cursorAt(0);
+		moveCursor(up());
+		assertCursorAt(0);
+	}
+
+	@Test
+	public void givenInFirstLineOfParagraph_whenMovingUp_shouldMoveCursorToParagraphStartOffset() throws Exception {
+		cursorAt(beginOfThirdParagraph() + 3);
+		moveCursor(up());
+		assertCursorAt(beginOfThirdParagraph());
+	}
+
+	@Test
+	public void givenRightBeforeFirstParagraphInSection_whenMovingUp_shouldMoveCursorToSectionStartOffset() throws Exception {
+		cursorAt(336);
+		moveCursor(up());
+		assertCursorAt(335);
+	}
+
+	@Test
+	public void givenAtSectionStartOffset_whenMovingUp_shouldMoveCursorToPreviousSectionEndOffset() throws Exception {
+		cursorAt(335);
+		moveCursor(up());
+		assertCursorAt(334);
+	}
+
+	@Test
+	public void givenAtSectionEndOffset_whenMovingUp_shouldMoveCursorToLastParagraphEndOffset() throws Exception {
+		cursorAt(endOfFirstSection());
+		moveCursor(up());
+		assertCursorAt(endOfSecondParagraph());
+	}
+
+	@Test
+	public void givenAtEmptyParagraphEndOffset_whenMovingUp_shouldMoveCursorToEmptyParagraphStartOffset() throws Exception {
+		cursorAt(endOfSecondParagraph());
+		moveCursor(up());
+		assertCursorAt(beginOfSecondParagraph());
+	}
+
+	@Test
+	public void givenBelowLastLineLeftOfLastCharacter_whenMovingUp_shouldMoveCursorAtPreferredXInLastLine() throws Exception {
+		cursorAt(340);
+		moveCursor(up());
+		moveCursor(up());
+		moveCursor(up());
+		moveCursor(up());
+		moveCursor(up());
+		moveCursor(up());
+		assertCursorAt(322);
+	}
+
+	@Test
+	public void givenBelowLastLineRightOfLastCharacter_whenMovingUp_shouldMoveCursorToEndOfParagraph() throws Exception {
+		cursorAt(360);
+		moveCursor(up());
+		moveCursor(up());
+		moveCursor(up());
+		moveCursor(up());
+		moveCursor(up());
+		moveCursor(up());
+		assertCursorAt(endOfFirstParagraph());
+	}
+
+	@Test
+	public void givenAtStartOfEmptyLine_whenMovingUp_shouldMoveCursorToStartOfLineAbove() throws Exception {
+		cursorAt(endOfSecondParagraph());
+		moveCursor(up());
+		moveCursor(up());
+		assertCursorAt(321);
+	}
+
+	@Test
+	public void givenAtFirstLineOfThirdParagraphSecondParagraphContainsText_whenMovingUp_shouldMoveIntoTextOfSecondParagraph() throws Exception {
+		document.getDocument().insertText(endOfSecondParagraph(), "lorem");
+		visualizeDocument();
+
+		cursorAt(beginOfThirdParagraph() + 4);
+		moveCursor(up());
+		moveCursor(up());
+		moveCursor(up());
+		moveCursor(up());
+		assertCursorAt(beginOfSecondParagraph() + 4);
+	}
+
+	@Test
+	public void givenInParagraphWithOnlyOneLine_whenMovingUp_shouldMoveToEndOfContainingSection() throws Exception {
+		document.getDocument().insertText(endOfSecondParagraph(), "lorem");
+		visualizeDocument();
+
+		cursorAt(beginOfSecondParagraph() + 4);
+		moveCursor(down());
+		assertCursorAt(endOfFirstSection());
+	}
+
+	@Test
+	public void givenInFirstLineOfFirstParagraph_whenMovingUp_shouldMoveCursorToStartOffsetOfFirstParagraph() throws Exception {
+		cursorAt(4);
+		moveCursor(up());
+		assertCursorAt(3);
+	}
+
+	@Test
+	public void givenAtStartOfFirstLineWithPreferredXZero_whenMovingUp_shouldMoveCursorToStartOfFirstParagraph() throws Exception {
+		cursorAt(0);
+		moveCursor(down());
+		moveCursor(down());
+		moveCursor(down());
+		moveCursor(down());
+		moveCursor(up());
+		assertCursorAt(3);
+	}
+
+	@Test
+	public void givenAtLastOffset_whenMovingUp_shouldMoveCursorToRootElementEndOffset() throws Exception {
+		cursorAt(lastOffset());
+		moveCursor(up());
+		assertCursorAt(document.getDocument().getRootElement().getEndOffset());
+	}
+
+	@Test
+	public void givenAtParagraphEndOffset_whenMovingUp_shouldMoveCursorToLineAbove() throws Exception {
+		cursorAt(endOfFirstParagraph());
+		moveCursor(up());
+		assertCursorAt(313);
+	}
+
+	@Test
+	public void canMoveCursorOneLineDown() throws Exception {
+		cursorAt(5);
+		moveCursor(down());
+		assertCursorAt(35);
+	}
+
+	@Test
+	public void whenAtLastOffset_cannotMoveCursorDown() throws Exception {
+		cursorAt(lastOffset());
+		cursor.move(down());
+		assertCursorAt(lastOffset());
+	}
+
+	@Test
+	public void givenInLastLineOfParagraph_whenMovingDown_shouldMoveCursorToNextParagraphStartOffset() throws Exception {
+		cursorAt(endOfFirstParagraph() - 2);
+		moveCursor(down());
+		assertCursorAt(beginOfSecondParagraph());
+	}
+
+	@Test
+	public void givenAtEndOfParagraph_whenMovingDown_shouldMoveCursorToNextParagraphStartOffset() throws Exception {
+		cursorAt(endOfFirstParagraph());
+		moveCursor(down());
+		assertCursorAt(beginOfSecondParagraph());
+	}
+
+	@Test
+	public void givenAtEndOfLastParagraphInSection_whenMovingDown_shouldMoveCursorToSectionEndOffset() throws Exception {
+		cursorAt(335);
+		moveCursor(down());
+		assertCursorAt(336);
+	}
+
+	@Test
+	public void givenInLineBeforeLastLineRightOfLastCharacterInLastLine_whenMovingDown_shouldMoveCursorToParagraphEndOffset() throws Exception {
+		cursorAt(317);
+		moveCursor(down());
+		assertCursorAt(endOfFirstParagraph());
+	}
+
+	@Test
+	public void givenAtSectionStartOffset_whenMovingDown_shouldMoveCursorToFirstParagraphStartOffset() throws Exception {
+		cursorAt(337);
+		moveCursor(down());
+		assertCursorAt(338);
+	}
+
+	@Test
+	public void givenAtDocumentStartOffset_whenMovingDown_shouldMoveCursorToRootElementStartOffset() throws Exception {
+		cursorAt(0);
+		moveCursor(down());
+		assertCursorAt(1);
+	}
+
+	@Test
+	public void givenAtStartOfFirstLineWithPreferredXZero_whenMovingDown_shouldMoveCursorToStartOfSecondLine() throws Exception {
+		cursorAt(0);
+		moveCursor(down());
+		moveCursor(down());
+		moveCursor(down());
+		moveCursor(down());
+		moveCursor(down());
+		assertCursorAt(34);
+	}
+
+	@Test
+	public void givenAtEndOfLastEmptyParagraph_whenMovingDown_shouldMoveCursorToLastSectionEndOffset() throws Exception {
+		cursorAt(endOfLastParagraph());
+		moveCursor(down());
+		assertCursorAt(endOfLastSection());
+	}
+
+	@Test
+	public void givenAtEndOfLongLine_whenMovingDown_shouldMoveCursorToEndOfShorterLineBelow() throws Exception {
+		cursorAt(149);
+		moveCursor(down());
+		assertCursorAt(170);
+	}
+
+	@Test
+	public void whenClickingIntoText_shouldMoveToPositionInText() throws Exception {
+		moveCursor(toAbsoluteCoordinates(18, 11));
+		assertCursorAt(5);
+	}
+
+	@Test
+	public void whenClickingRightOfLastLine_shouldMoveToEndOfParagraph() throws Exception {
+		moveCursor(toAbsoluteCoordinates(133, 168));
+		assertCursorAt(endOfFirstParagraph());
+	}
+
+	@Test
+	public void whenClickingLeftOfLine_shouldMoveToBeginningOfLine() throws Exception {
+		for (int x = 0; x < 10; x += 1) {
+			cursorAt(0);
+			moveCursor(toAbsoluteCoordinates(x, 11));
+			assertCursorAt("x=" + x, 4);
+		}
+	}
+
+	@Test
+	public void whenClickingRightOfLine_shouldMoveToEndOfLine() throws Exception {
+		for (int x = 199; x > 193; x -= 1) {
+			cursorAt(0);
+			moveCursor(toAbsoluteCoordinates(x, 11));
+			assertCursorAt("x=" + x, 33);
+		}
+	}
+
+	@Test
+	public void whenClickingInEmptyLine_shouldMoveToEndOfParagraph() throws Exception {
+		moveCursor(toAbsoluteCoordinates(10, 187));
+		assertCursorAt(endOfSecondParagraph());
+	}
+
+	@Test
+	public void whenClickingBelowLastLine_shouldMoveToEndOfParagraph() throws Exception {
+		for (int x = 6; x < 194; x += 1) {
+			cursorAt(0);
+			moveCursor(toAbsoluteCoordinates(x, 181));
+			assertCursorAt("x=" + x, endOfFirstParagraph());
+		}
+	}
+
+	@Test
+	public void whenClickingInLastEmptyParagraph_shouldMoveToEndOfParagraph() throws Exception {
+		cursorAt(0);
+		moveCursor(toAbsoluteCoordinates(10, 395));
+		assertCursorAt(document.getEmptyParagraph(1).getEndOffset());
+	}
+
+	@Test
+	public void whenCursorMoves_shouldFirePositionChanged() throws Exception {
+		final int[] announcedOffset = new int[1];
+		announcedOffset[0] = -1;
+		cursor.addPositionListener(new ICursorPositionListener() {
+			@Override
+			public void positionChanged(final int offset) {
+				announcedOffset[0] = offset;
+			}
+
+			@Override
+			public void positionAboutToChange() {
+			}
+		});
+
+		cursorAt(123);
+		moveCursor(right());
+
+		assertEquals(cursor.getOffset(), announcedOffset[0]);
+	}
+
+	@Test
+	public void whenCursorMoveIsProhibited_shouldNotFirePositionChanged() throws Exception {
+		final boolean[] receivedPositionChangedMessage = new boolean[1];
+		receivedPositionChangedMessage[0] = false;
+		cursor.addPositionListener(new ICursorPositionListener() {
+			@Override
+			public void positionChanged(final int offset) {
+				receivedPositionChangedMessage[0] = true;
+			}
+
+			@Override
+			public void positionAboutToChange() {
+			}
+		});
+
+		cursorAt(0);
+		moveCursor(left());
+
+		assertFalse(receivedPositionChangedMessage[0]);
+	}
+
+	@Test
+	public void moveToNextWord() throws Exception {
+		cursorAt(beginOfFirstText()); //|0 Lorem
+		moveCursor(toNextWord());
+		assertCursorAt(beginOfFirstText() + 2); //0 |Lorem
+	}
+
+	@Test
+	public void moveToPreviousWord() throws Exception {
+		cursorAt(endOfFirstText() - 1); // consectur|.
+		moveCursor(toPreviousWord());
+		assertCursorAt(endOfFirstText() - 12); //lacinia |consectur.
+	}
+
+	/*
+	 * Utility methods
+	 */
+
+	private void cursorAt(final int offset) {
+		moveCursor(toOffset(offset));
+	}
+
+	private void moveCursor(final ICursorMove move) {
+		cursor.move(move);
+		cursor.applyMoves(graphics);
+		cursor.paint(graphics);
+	}
+
+	private void assertCursorAt(final int offset) {
+		cursor.applyMoves(graphics);
+		assertEquals(offset, cursor.getOffset());
+	}
+
+	private void assertCursorAt(final String message, final int offset) {
+		cursor.applyMoves(graphics);
+		assertEquals(message, offset, cursor.getOffset());
+	}
+
+	private static VisualizationChain buildVisualizationChain() {
+		final VisualizationChain visualizationChain = new VisualizationChain();
+		visualizationChain.addForRoot(new DocumentRootVisualization());
+		visualizationChain.addForStructure(new ParagraphVisualization());
+		visualizationChain.addForStructure(new StructureElementVisualization());
+		visualizationChain.addForInline(new TextVisualization());
+		return visualizationChain;
+	}
+
+	private int beginOfFirstText() {
+		return document.getParagraphWithText(0).getStartOffset() + 1;
+	}
+
+	private int endOfFirstText() {
+		return endOfFirstParagraph();
+	}
+
+	private int endOfFirstParagraph() {
+		return document.getParagraphWithText(0).getEndOffset();
+	}
+
+	private int beginOfSecondParagraph() {
+		return document.getEmptyParagraph(0).getStartOffset();
+	}
+
+	private int endOfSecondParagraph() {
+		return document.getEmptyParagraph(0).getEndOffset();
+	}
+
+	private int endOfFirstSection() {
+		return document.getSection(0).getEndOffset();
+	}
+
+	private int beginOfThirdParagraph() {
+		return document.getParagraphWithText(1).getStartOffset();
+	}
+
+	private int endOfLastParagraph() {
+		return document.getEmptyParagraph(1).getEndOffset();
+	}
+
+	private int endOfLastSection() {
+		return document.getSection(1).getEndOffset();
+	}
+
+	private int lastOffset() {
+		return document.getDocument().getEndOffset();
+	}
+
+	/*
+	 * For visualization of the box structure:
+	 */
+	@SuppressWarnings("unused")
+	private void printBoxStructure() {
+		rootBox.accept(new DepthFirstBoxTraversal<Object>() {
+			private String indent = "";
+
+			@Override
+			public Object visit(final StructuralNodeReference box) {
+				printBox(box);
+				indent += " ";
+				super.visit(box);
+				indent = indent.substring(1);
+				return null;
+			}
+
+			@Override
+			public Object visit(final InlineNodeReference box) {
+				printBox(box);
+				indent += " ";
+				super.visit(box);
+				indent = indent.substring(1);
+				return null;
+			}
+
+			@Override
+			public Object visit(final TextContent box) {
+				printBox(box);
+				return null;
+			}
+
+			@Override
+			public Object visit(final NodeEndOffsetPlaceholder box) {
+				printBox(box);
+				return null;
+			}
+
+			private void printBox(final IContentBox box) {
+				System.out.println(indent + "[" + box.getAbsoluteLeft() + ". " + box.getAbsoluteTop() + ", " + box.getWidth() + ", " + box.getHeight() + "] [" + box.getStartOffset() + ", "
+						+ box.getEndOffset() + "]");
+			}
+		});
+	}
+}
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/dom/ContentTest.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/dom/ContentTest.java
index 9a92e9d..73739cc 100644
--- a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/dom/ContentTest.java
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/dom/ContentTest.java
@@ -17,6 +17,7 @@
 import org.eclipse.vex.core.provisional.dom.ContentRange;

 import org.eclipse.vex.core.provisional.dom.IContent;

 import org.eclipse.vex.core.provisional.dom.IPosition;

+import org.eclipse.vex.core.provisional.dom.MultilineText;

 import org.junit.Before;

 import org.junit.Test;

 

@@ -236,4 +237,51 @@
 		assertTrue("after", positionAfter.isValid());

 	}

 

+	@Test

+	public void givenTextWithLineBreaks_shouldProvideLinesWithRanges() throws Exception {

+		content.insertText(0, "line 1\nline 2\nline 3");

+

+		final MultilineText multilineText = content.getMultilineText(content.getRange());

+

+		assertEquals(3, multilineText.size());

+		assertEquals("line 1\n", multilineText.getText(0));

+		assertEquals(new ContentRange(0, 6), multilineText.getRange(0));

+		assertEquals("line 2\n", multilineText.getText(1));

+		assertEquals(new ContentRange(7, 13), multilineText.getRange(1));

+		assertEquals("line 3", multilineText.getText(2));

+		assertEquals(new ContentRange(14, 19), multilineText.getRange(2));

+	}

+

+	@Test

+	public void givenTextWithoutLineBreaks_shouldProvideSingleLineWithRange() throws Exception {

+		content.insertText(0, "line 1");

+

+		final MultilineText multilineText = content.getMultilineText(content.getRange());

+

+		assertEquals(1, multilineText.size());

+		assertEquals("line 1", multilineText.getText(0));

+		assertEquals(new ContentRange(0, 5), multilineText.getRange(0));

+	}

+

+	@Test

+	public void givenTextWithLineBreaks_whenTextEndsWithLineBreak_shouldNotProvideEmptyLineAsLastLine() throws Exception {

+		content.insertText(0, "line 1\nline 2\nline 3\n");

+

+		final MultilineText multilineText = content.getMultilineText(content.getRange());

+

+		assertEquals(3, multilineText.size());

+		assertEquals("line 3\n", multilineText.getText(2));

+		assertEquals(new ContentRange(14, 20), multilineText.getRange(2));

+	}

+

+	@Test

+	public void allowToInsertALineBreak() throws Exception {

+		content.insertText(0, "line 1line 2");

+		content.insertLineBreak(7);

+

+		final MultilineText multilineText = content.getMultilineText(content.getRange());

+

+		assertEquals(2, multilineText.size());

+	}

+

 }

diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/dom/DocumentEventTest.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/dom/DocumentEventTest.java
index 088b4fc..f1bc12e 100644
--- a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/dom/DocumentEventTest.java
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/dom/DocumentEventTest.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2013 Carsten Hiesserich and others.
+ * Copyright (c) 2013, 2015 Carsten Hiesserich 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
@@ -75,7 +75,15 @@
 	}
 
 	@Test
-	public void testInsertText() throws Exception {
+	public void givenEmptyElement_whenInsertingText_shouldIndicateStructuralChange() throws Exception {
+		document.insertText(childNode.getEndOffset(), "Hello World");
+		assertNotNull("Expecting ContentChangeEvent", contentChangeEvent);
+		assertTrue("Expecting structural change", contentChangeEvent.isStructuralChange());
+	}
+
+	@Test
+	public void givenElementWithText_whenInsertingText_shouldNotIndicateStructuralChange() throws Exception {
+		document.insertText(childNode.getEndOffset(), "Some Text");
 		document.insertText(childNode.getEndOffset(), "Hello World");
 		assertNotNull("Expecting ContentChangeEvent", contentChangeEvent);
 		assertFalse("Expecting no structural change", contentChangeEvent.isStructuralChange());
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/layout/FakeGraphics.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/layout/FakeGraphics.java
index 856af9e..c765b76 100644
--- a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/layout/FakeGraphics.java
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/layout/FakeGraphics.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2004, 2010 John Krasnay and others.
+ * Copyright (c) 2004, 2015 John Krasnay 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
@@ -22,6 +22,7 @@
 import org.eclipse.vex.core.internal.core.FontSpec;
 import org.eclipse.vex.core.internal.core.Graphics;
 import org.eclipse.vex.core.internal.core.Image;
+import org.eclipse.vex.core.internal.core.LineStyle;
 import org.eclipse.vex.core.internal.core.Rectangle;
 
 /**
@@ -69,13 +70,39 @@
 		}
 	};
 
+	public void resetOrigin() {
+	}
+
+	public void moveOrigin(final int offsetX, final int offsetY) {
+	}
+
+	@Override
+	public int asAbsoluteX(final int relativeX) {
+		return relativeX;
+	}
+
+	@Override
+	public int asAbsoluteY(final int relativeY) {
+		return relativeY;
+	}
+
+	@Override
+	public int asRelativeX(final int absoluteX) {
+		return absoluteX;
+	}
+
+	@Override
+	public int asRelativeY(final int absoluteY) {
+		return absoluteY;
+	}
+
 	@Override
 	public int charsWidth(final char[] data, final int offset, final int length) {
 		return length * charWidth;
 	}
 
 	@Override
-	public ColorResource createColor(final Color rgb) {
+	public ColorResource getColor(final Color rgb) {
 		return new ColorResource() {
 			@Override
 			public void dispose() {
@@ -84,7 +111,7 @@
 	}
 
 	@Override
-	public FontResource createFont(final FontSpec fontSpec) {
+	public FontResource getFont(final FontSpec fontSpec) {
 		return new FontResource() {
 			@Override
 			public void dispose() {
@@ -117,6 +144,14 @@
 	}
 
 	@Override
+	public void drawRoundRect(final int x, final int y, final int width, final int height, final int arcWidth, final int arcHeight) {
+	}
+
+	@Override
+	public void drawPolygon(final int... coordinates) {
+	}
+
+	@Override
 	public void drawImage(final Image image, final int x, final int y, final int width, final int height) {
 		Assert.isTrue(image instanceof FakeImage);
 		lastDrawnImageUrl = ((FakeImage) image).url;
@@ -135,6 +170,14 @@
 	}
 
 	@Override
+	public void fillRoundRect(final int x, final int y, final int width, final int height, final int arcWidth, final int arcHeight) {
+	}
+
+	@Override
+	public void fillPolygon(final int... coordinates) {
+	}
+
+	@Override
 	public Rectangle getClipBounds() {
 		return new Rectangle(0, 0, Integer.MAX_VALUE, Integer.MAX_VALUE);
 	}
@@ -144,18 +187,13 @@
 	}
 
 	@Override
-	public ColorResource getColor() {
+	public FontResource getCurrentFont() {
 		return null;
 	}
 
 	@Override
-	public FontResource getFont() {
-		return null;
-	}
-
-	@Override
-	public int getLineStyle() {
-		return 0;
+	public LineStyle getLineStyle() {
+		return LineStyle.SOLID;
 	}
 
 	@Override
@@ -192,17 +230,46 @@
 	}
 
 	@Override
+	public ColorResource getColor() {
+		return null;
+	}
+
+	@Override
 	public ColorResource setColor(final ColorResource color) {
 		return null;
 	}
 
 	@Override
-	public FontResource setFont(final FontResource font) {
+	public ColorResource getForeground() {
 		return null;
 	}
 
 	@Override
-	public void setLineStyle(final int style) {
+	public ColorResource setForeground(final ColorResource color) {
+		return null;
+	}
+
+	@Override
+	public ColorResource getBackground() {
+		return null;
+	}
+
+	@Override
+	public ColorResource setBackground(final ColorResource color) {
+		return null;
+	}
+
+	@Override
+	public void swapColors() {
+	}
+
+	@Override
+	public FontResource setCurrentFont(final FontResource font) {
+		return null;
+	}
+
+	@Override
+	public void setLineStyle(final LineStyle style) {
 	}
 
 	@Override
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/layout/endtoend/EndToEndTest.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/layout/endtoend/EndToEndTest.java
index b9dcc03..81f32a4 100644
--- a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/layout/endtoend/EndToEndTest.java
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/layout/endtoend/EndToEndTest.java
@@ -15,7 +15,6 @@
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.OutputStream;
 import java.io.PrintStream;
 
 import javax.xml.parsers.ParserConfigurationException;
@@ -40,20 +39,6 @@
  */
 public class EndToEndTest {
 
-	private static final String UNIX_SEPARATOR = "\n";
-	private static final String LINE_SEPARATOR = "line.separator";
-	private static final DisplayDevice DD_72DPI = new DisplayDevice() {
-		@Override
-		public int getHorizontalPPI() {
-			return 72;
-		}
-
-		@Override
-		public int getVerticalPPI() {
-			return 72;
-		}
-	};
-
 	@Rule
 	public TestName name = new TestName();
 
@@ -74,22 +59,14 @@
 
 	private static String traceRendering(final IDocument document, final StyleSheet styleSheet) throws IOException, ParserConfigurationException, SAXException {
 		final ByteArrayOutputStream traceBuffer = new ByteArrayOutputStream();
-		final PrintStream printStream = createUnixPrintStream(traceBuffer);
+		final PrintStream printStream = new PrintStream(traceBuffer);
 
-		DisplayDevice.setCurrent(DD_72DPI);
+		DisplayDevice.setCurrent(DisplayDevice._72DPI);
 		final TracingHostComponent hostComponent = new TracingHostComponent(printStream);
 		final BaseVexWidget widget = new BaseVexWidget(hostComponent);
 		widget.setDocument(document, styleSheet);
 		widget.paint(hostComponent.createDefaultGraphics(), 0, 0);
-		return new String(traceBuffer.toByteArray());
-	}
-
-	private static PrintStream createUnixPrintStream(final OutputStream target) {
-		final String originalSeparator = System.getProperty(LINE_SEPARATOR);
-		System.setProperty(LINE_SEPARATOR, UNIX_SEPARATOR);
-		final PrintStream printStream = new PrintStream(target, true);
-		System.setProperty(LINE_SEPARATOR, originalSeparator);
-		return printStream;
+		return normalizeLineSeparators(new String(traceBuffer.toByteArray()));
 	}
 
 	private StyleSheet readStyleSheet() throws IOException {
@@ -109,8 +86,12 @@
 		return reader.read(getClass().getResource(inputName()));
 	}
 
+	private static String normalizeLineSeparators(final String s) {
+		return s.replaceAll("[\n\r]+", "\n");
+	}
+
 	private String readExpectedTrace() throws IOException {
-		return readAndCloseStream(getClass().getResourceAsStream(outputName()));
+		return normalizeLineSeparators(readAndCloseStream(getClass().getResourceAsStream(outputName())));
 	}
 
 	private static String readAndCloseStream(final InputStream in) throws IOException {
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/layout/endtoend/TracingGraphics.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/layout/endtoend/TracingGraphics.java
index 6a16725..bb6331c 100644
--- a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/layout/endtoend/TracingGraphics.java
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/layout/endtoend/TracingGraphics.java
@@ -19,6 +19,7 @@
 import org.eclipse.vex.core.internal.core.FontSpec;
 import org.eclipse.vex.core.internal.core.Graphics;
 import org.eclipse.vex.core.internal.core.Image;
+import org.eclipse.vex.core.internal.core.LineStyle;
 import org.eclipse.vex.core.internal.core.Rectangle;
 import org.eclipse.vex.core.internal.layout.FakeImage;
 
@@ -55,27 +56,59 @@
 
 	private boolean antiAliased;
 	private int lineWidth;
-	private int lineStyle;
+	private LineStyle lineStyle;
 	private FontResource font;
 	private ColorResource color;
+	private ColorResource foreground;
+	private ColorResource background;
 
 	public TracingGraphics(final Tracer tracer) {
 		this.tracer = tracer;
 	}
 
 	@Override
+	public void resetOrigin() {
+		tracer.trace("Graphics.resetOrigin()");
+	}
+
+	@Override
+	public void moveOrigin(final int offsetX, final int offsetY) {
+		tracer.trace("Graphics.moveOrigin({0}, {1})", offsetX, offsetY);
+	}
+
+	@Override
+	public int asAbsoluteX(final int relativeX) {
+		return relativeX;
+	}
+
+	@Override
+	public int asAbsoluteY(final int relativeY) {
+		return relativeY;
+	}
+
+	@Override
+	public int asRelativeX(final int absoluteX) {
+		return absoluteX;
+	}
+
+	@Override
+	public int asRelativeY(final int absoluteY) {
+		return absoluteY;
+	}
+
+	@Override
 	public int charsWidth(final char[] data, final int offset, final int length) {
 		return CHAR_WIDTH * length;
 	}
 
 	@Override
-	public ColorResource createColor(final Color color) {
+	public ColorResource getColor(final Color color) {
 		tracer.trace("Graphics.createColor({0})", color);
 		return new TracingColorResource(tracer, color);
 	}
 
 	@Override
-	public FontResource createFont(final FontSpec fontSpec) {
+	public FontResource getFont(final FontSpec fontSpec) {
 		tracer.trace("Graphics.createFont({0})", fontSpec);
 		return new TracingFontResource(tracer, fontSpec);
 	}
@@ -107,7 +140,16 @@
 
 	@Override
 	public void drawRect(final int x, final int y, final int width, final int height) {
-		tracer.trace("Graphics.drawRect({0,number,#}, {1,number,#}, {2,number,#}, {3,number,#})", x, y, width, height);
+	}
+
+	@Override
+	public void drawRoundRect(final int x, final int y, final int width, final int height, final int arcWidth, final int arcHeight) {
+		tracer.trace("Graphics.drawRoundRect({0,number,#}, {1,number,#}, {2,number,#}, {3,number,#}, {4,number,#}, {5,number,#})", x, y, width, height, arcWidth, arcHeight);
+	}
+
+	@Override
+	public void drawPolygon(final int... coordinates) {
+		tracer.trace("Graphics.drawPolygon(int[{0}])", coordinates.length);
 	}
 
 	@Override
@@ -126,22 +168,27 @@
 	}
 
 	@Override
+	public void fillRoundRect(final int x, final int y, final int width, final int height, final int arcWidth, final int arcHeight) {
+		tracer.trace("Graphics.fillRoundRect({0,number,#}, {1,number,#}, {2,number,#}, {3,number,#}, {4,number,#}, {5,number,#})", x, y, width, height, arcWidth, arcHeight);
+	}
+
+	@Override
+	public void fillPolygon(final int... coordinates) {
+		tracer.trace("Graphics.fillPolygon(int[{0}])", coordinates.length);
+	}
+
+	@Override
 	public Rectangle getClipBounds() {
 		return new Rectangle(0, 0, Integer.MAX_VALUE, Integer.MAX_VALUE);
 	}
 
 	@Override
-	public ColorResource getColor() {
-		return color;
-	}
-
-	@Override
-	public FontResource getFont() {
+	public FontResource getCurrentFont() {
 		return font;
 	}
 
 	@Override
-	public int getLineStyle() {
+	public LineStyle getLineStyle() {
 		return lineStyle;
 	}
 
@@ -179,6 +226,11 @@
 	}
 
 	@Override
+	public ColorResource getColor() {
+		return color;
+	}
+
+	@Override
 	public ColorResource setColor(final ColorResource color) {
 		tracer.trace("Graphics.setColor({0})", color);
 		final ColorResource oldColor = getColor();
@@ -187,15 +239,46 @@
 	}
 
 	@Override
-	public FontResource setFont(final FontResource font) {
+	public ColorResource getForeground() {
+		return foreground;
+	}
+
+	@Override
+	public ColorResource setForeground(final ColorResource color) {
+		tracer.trace("Graphics.setForeground({0})", color);
+		final ColorResource oldColor = getForeground();
+		foreground = color;
+		return oldColor;
+	}
+
+	@Override
+	public ColorResource getBackground() {
+		return background;
+	}
+
+	@Override
+	public ColorResource setBackground(final ColorResource color) {
+		tracer.trace("Graphics.setBackground({0})", color);
+		final ColorResource oldColor = getBackground();
+		background = color;
+		return oldColor;
+	}
+
+	@Override
+	public void swapColors() {
+		tracer.trace("Graphics.swapColors()");
+	}
+
+	@Override
+	public FontResource setCurrentFont(final FontResource font) {
 		tracer.trace("Graphics.setFont({0})", font);
-		final FontResource oldFont = getFont();
+		final FontResource oldFont = getCurrentFont();
 		this.font = font;
 		return oldFont;
 	}
 
 	@Override
-	public void setLineStyle(final int style) {
+	public void setLineStyle(final LineStyle style) {
 		tracer.trace("Graphics.setLineStyle({0})", style);
 		lineStyle = style;
 	}
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/layout/endtoend/simpleParagraph-output.txt b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/layout/endtoend/simpleParagraph-output.txt
index 95daf08..57050be 100644
--- a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/layout/endtoend/simpleParagraph-output.txt
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/layout/endtoend/simpleParagraph-output.txt
@@ -7,47 +7,36 @@
 Graphics.createFont(FontSpec [names=[monospaced], size=10.0, style=PLAIN])
 Graphics.setFont(TracingFontResource [fontSpec=FontSpec [names=[monospaced], size=10.0, style=PLAIN]])
 Graphics.setFont(null)
-FontResource[FontSpec [names=[monospaced], size=10.0, style=PLAIN]].dispose()
 Graphics.createFont(FontSpec [names=[monospaced], size=10.0, style=PLAIN])
 Graphics.setFont(TracingFontResource [fontSpec=FontSpec [names=[monospaced], size=10.0, style=PLAIN]])
 Graphics.setFont(null)
-FontResource[FontSpec [names=[monospaced], size=10.0, style=PLAIN]].dispose()
 Graphics.createFont(FontSpec [names=[monospaced], size=10.0, style=PLAIN])
 Graphics.setFont(TracingFontResource [fontSpec=FontSpec [names=[monospaced], size=10.0, style=PLAIN]])
 Graphics.setFont(null)
-FontResource[FontSpec [names=[monospaced], size=10.0, style=PLAIN]].dispose()
 Graphics.createFont(FontSpec [names=[monospaced], size=10.0, style=PLAIN])
 Graphics.setFont(TracingFontResource [fontSpec=FontSpec [names=[monospaced], size=10.0, style=PLAIN]])
 Graphics.setFont(null)
-FontResource[FontSpec [names=[monospaced], size=10.0, style=PLAIN]].dispose()
 Graphics.createFont(FontSpec [names=[monospaced], size=10.0, style=PLAIN])
 Graphics.setFont(TracingFontResource [fontSpec=FontSpec [names=[monospaced], size=10.0, style=PLAIN]])
 Graphics.setFont(null)
-FontResource[FontSpec [names=[monospaced], size=10.0, style=PLAIN]].dispose()
 Graphics.createFont(FontSpec [names=[monospaced], size=10.0, style=PLAIN])
 Graphics.setFont(TracingFontResource [fontSpec=FontSpec [names=[monospaced], size=10.0, style=PLAIN]])
 Graphics.setFont(null)
-FontResource[FontSpec [names=[monospaced], size=10.0, style=PLAIN]].dispose()
 Graphics.createFont(FontSpec [names=[monospaced], size=10.0, style=PLAIN])
 Graphics.setFont(TracingFontResource [fontSpec=FontSpec [names=[monospaced], size=10.0, style=PLAIN]])
 Graphics.setFont(null)
-FontResource[FontSpec [names=[monospaced], size=10.0, style=PLAIN]].dispose()
 Graphics.createFont(FontSpec [names=[monospaced], size=10.0, style=PLAIN])
 Graphics.setFont(TracingFontResource [fontSpec=FontSpec [names=[monospaced], size=10.0, style=PLAIN]])
 Graphics.setFont(null)
-FontResource[FontSpec [names=[monospaced], size=10.0, style=PLAIN]].dispose()
 Graphics.createFont(FontSpec [names=[monospaced], size=10.0, style=PLAIN])
 Graphics.setFont(TracingFontResource [fontSpec=FontSpec [names=[monospaced], size=10.0, style=PLAIN]])
 Graphics.setFont(null)
-FontResource[FontSpec [names=[monospaced], size=10.0, style=PLAIN]].dispose()
 Graphics.createFont(FontSpec [names=[monospaced], size=10.0, style=PLAIN])
 Graphics.setFont(TracingFontResource [fontSpec=FontSpec [names=[monospaced], size=10.0, style=PLAIN]])
 Graphics.setFont(null)
-FontResource[FontSpec [names=[monospaced], size=10.0, style=PLAIN]].dispose()
 Graphics.createFont(FontSpec [names=[monospaced], size=10.0, style=PLAIN])
 Graphics.setFont(TracingFontResource [fontSpec=FontSpec [names=[monospaced], size=10.0, style=PLAIN]])
 Graphics.setFont(null)
-FontResource[FontSpec [names=[monospaced], size=10.0, style=PLAIN]].dispose()
 Graphics.dispose()
 HostComponent.getViewport()
 HostComponent.repaint(0, 0, 2147483647, 36)
@@ -75,8 +64,6 @@
 Graphics.drawChars(The quick brown , 0, 16, 0, 0)
 Graphics.setFont(null)
 Graphics.setColor(null)
-FontResource[FontSpec [names=[monospaced], size=10.0, style=PLAIN]].dispose()
-ColorResource[Color[r=0,g=0,b=0], -1].dispose()
 Graphics.createFont(FontSpec [names=[monospaced], size=10.0, style=PLAIN])
 Graphics.setFont(TracingFontResource [fontSpec=FontSpec [names=[monospaced], size=10.0, style=PLAIN]])
 Graphics.createColor(Color[r=0,g=0,b=0])
@@ -84,8 +71,6 @@
 Graphics.drawChars(fox , 0, 4, 96, 0)
 Graphics.setFont(null)
 Graphics.setColor(null)
-FontResource[FontSpec [names=[monospaced], size=10.0, style=PLAIN]].dispose()
-ColorResource[Color[r=0,g=0,b=0], -1].dispose()
 Graphics.setAntialiased(false)
 Graphics.setAntialiased(false)
 Graphics.createFont(FontSpec [names=[monospaced], size=10.0, style=PLAIN])
@@ -95,8 +80,6 @@
 Graphics.drawChars(jumps, 0, 5, 0, 12)
 Graphics.setFont(null)
 Graphics.setColor(null)
-FontResource[FontSpec [names=[monospaced], size=10.0, style=PLAIN]].dispose()
-ColorResource[Color[r=0,g=0,b=0], -1].dispose()
 Graphics.createFont(FontSpec [names=[monospaced], size=10.0, style=PLAIN])
 Graphics.setFont(TracingFontResource [fontSpec=FontSpec [names=[monospaced], size=10.0, style=PLAIN]])
 Graphics.createColor(Color[r=0,g=0,b=0])
@@ -104,8 +87,6 @@
 Graphics.drawChars( over the lazy dog's , 0, 21, 0, 24)
 Graphics.setFont(null)
 Graphics.setColor(null)
-FontResource[FontSpec [names=[monospaced], size=10.0, style=PLAIN]].dispose()
-ColorResource[Color[r=0,g=0,b=0], -1].dispose()
 Graphics.createFont(FontSpec [names=[monospaced], size=10.0, style=PLAIN])
 Graphics.setFont(TracingFontResource [fontSpec=FontSpec [names=[monospaced], size=10.0, style=PLAIN]])
 Graphics.createColor(Color[r=0,g=0,b=0])
@@ -113,10 +94,7 @@
 Graphics.drawChars(tail., 0, 5, 126, 24)
 Graphics.setFont(null)
 Graphics.setColor(null)
-FontResource[FontSpec [names=[monospaced], size=10.0, style=PLAIN]].dispose()
-ColorResource[Color[r=0,g=0,b=0], -1].dispose()
 Graphics.createColor(Color[r=0,g=0,b=0])
 Graphics.setColor(TracingColorResource [color=Color[r=0,g=0,b=0], id=-1])
 Graphics.fillRect(0, 0, 20, 2)
 Graphics.setColor(null)
-ColorResource[Color[r=0,g=0,b=0], -1].dispose()
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/undo/EditStackTest.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/undo/EditStackTest.java
new file mode 100644
index 0000000..d77453c
--- /dev/null
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/undo/EditStackTest.java
@@ -0,0 +1,333 @@
+/*******************************************************************************
+ * 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.undo;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+/**
+ * @author Florian Thienel
+ */
+public class EditStackTest {
+
+	@Test
+	public void apply_shouldIndicateUndoPossible() throws Exception {
+		final EditStack stack = new EditStack();
+
+		stack.apply(new MockEdit());
+
+		assertTrue("can undo", stack.canUndo());
+	}
+
+	@Test
+	public void apply_shouldPerformRedo() throws Exception {
+		final EditStack stack = new EditStack();
+		final MockEdit edit = new MockEdit();
+
+		stack.apply(edit);
+
+		assertTrue("perform redo", edit.redoCalled);
+	}
+
+	@Test
+	public void apply_whenEditCannotBeUndone_shouldIndicateUndoNotPossible() throws Exception {
+		final EditStack stack = new EditStack();
+
+		stack.apply(new MockEdit(false, false));
+
+		assertFalse("cannot undo", stack.canUndo());
+	}
+
+	@Test
+	public void apply_when3EditsWhereUndone_shouldDiscardUndoneEdits() throws Exception {
+		final EditStack stack = new EditStack();
+
+		stack.apply(new MockEdit());
+		stack.apply(new MockEdit());
+		stack.apply(new MockEdit());
+		stack.undo();
+		stack.undo();
+		stack.undo();
+		stack.apply(new MockEdit());
+
+		assertFalse("discard undone edits", stack.canRedo());
+	}
+
+	@Test
+	public void apply_shouldCombineEditsIfPossible() throws Exception {
+		final EditStack stack = new EditStack();
+
+		final MockEdit edit1 = stack.apply(new MockEdit());
+		edit1.canCombine = true;
+
+		stack.apply(new MockEdit());
+
+		assertTrue("combine", edit1.combineCalled);
+	}
+
+	@Test
+	public void undo_shouldPerformUndo() throws Exception {
+		final EditStack stack = new EditStack();
+		final MockEdit edit = new MockEdit();
+
+		stack.apply(edit);
+		stack.undo();
+
+		assertTrue("perform undo", edit.undoCalled);
+	}
+
+	@Test(expected = CannotUndoException.class)
+	public void undo_whenStackIsEmpty_shouldThrowCannotUndoException() throws Exception {
+		new EditStack().undo();
+	}
+
+	@Test
+	public void undo_whenGiven3EditsAndCallingUndo3Times_shouldPerformUndoOnAllEdits() throws Exception {
+		final EditStack stack = new EditStack();
+		final MockEdit edit1 = new MockEdit();
+		final MockEdit edit2 = new MockEdit();
+		final MockEdit edit3 = new MockEdit();
+
+		stack.apply(edit1);
+		stack.apply(edit2);
+		stack.apply(edit3);
+		stack.undo();
+		stack.undo();
+		stack.undo();
+
+		assertTrue("undo edit1", edit1.undoCalled);
+		assertTrue("undo edit2", edit2.undoCalled);
+		assertTrue("undo edit3", edit3.undoCalled);
+	}
+
+	@Test
+	public void undo_whenGiven3EditsAndCallingUndo3Times_shouldPerformUndoInReverseOrder() throws Exception {
+		final EditStack stack = new EditStack();
+		final MockEdit edit1 = new MockEdit();
+		final MockEdit edit2 = new MockEdit();
+		final MockEdit edit3 = new MockEdit();
+
+		stack.apply(edit1);
+		stack.apply(edit2);
+		stack.apply(edit3);
+
+		stack.undo();
+		assertTrue("undo edit3", edit3.undoCalled);
+
+		stack.undo();
+		assertTrue("undo edit2", edit2.undoCalled);
+
+		stack.undo();
+		assertTrue("undo edit1", edit1.undoCalled);
+	}
+
+	@Test
+	public void redo_shouldPerformRedoOnLastUndoneEdit() throws Exception {
+		final EditStack stack = new EditStack();
+		final MockEdit edit = new MockEdit();
+
+		stack.apply(edit);
+		stack.undo();
+
+		edit.redoCalled = false;
+		stack.redo();
+
+		assertTrue("perform redo", edit.redoCalled);
+	}
+
+	@Test(expected = CannotApplyException.class)
+	public void redo_whenNothingWasUndone_shouldThrowCannotApplyException() throws Exception {
+		new EditStack().redo();
+	}
+
+	@Test
+	public void commit_shouldApplyAllPendingEdits() throws Exception {
+		final EditStack stack = new EditStack();
+		final MockEdit edit1 = new MockEdit();
+		final MockEdit edit2 = new MockEdit();
+		final MockEdit edit3 = new MockEdit();
+
+		stack.beginWork();
+		stack.apply(edit1);
+		stack.apply(edit2);
+		stack.apply(edit3);
+		stack.commitWork();
+
+		assertTrue("apply edit1", edit1.redoCalled);
+		assertTrue("apply edit2", edit2.redoCalled);
+		assertTrue("apply edit3", edit3.redoCalled);
+	}
+
+	@Test
+	public void rollback_shouldUndoAllPendingEdits() throws Exception {
+		final EditStack stack = new EditStack();
+		final MockEdit edit1 = new MockEdit();
+		final MockEdit edit2 = new MockEdit();
+		final MockEdit edit3 = new MockEdit();
+
+		stack.beginWork();
+		stack.apply(edit1);
+		stack.apply(edit2);
+		stack.apply(edit3);
+		stack.rollbackWork();
+
+		assertTrue("undo edit1", edit1.undoCalled);
+		assertTrue("undo edit2", edit2.undoCalled);
+		assertTrue("undo edit3", edit3.undoCalled);
+	}
+
+	@Test
+	public void inTransaction_whenOpendTwiceButCommittedOnlyOnce_shouldIndicateTrue() throws Exception {
+		final EditStack stack = new EditStack();
+
+		stack.beginWork();
+		stack.beginWork();
+		stack.commitWork();
+
+		assertTrue("in transaction", stack.inTransaction());
+	}
+
+	@Test
+	public void nestedTransactions_whenNestedTransactionIsRolledBackAndOuterTransactionIsCommitted_shouldApplyEditsInOuterTransaction() throws Exception {
+		final EditStack stack = new EditStack();
+		final MockEdit edit1 = new MockEdit();
+		final MockEdit edit2 = new MockEdit();
+		final MockEdit edit3 = new MockEdit();
+		final MockEdit edit4 = new MockEdit();
+		final MockEdit edit5 = new MockEdit();
+		final MockEdit edit6 = new MockEdit();
+
+		stack.beginWork();
+		stack.apply(edit1);
+		stack.apply(edit2);
+		stack.apply(edit3);
+		stack.beginWork();
+		stack.apply(edit4);
+		stack.apply(edit5);
+		stack.apply(edit6);
+		stack.rollbackWork();
+		stack.commitWork();
+
+		assertFalse("apply edit1", edit1.undoCalled);
+		assertFalse("apply edit2", edit2.undoCalled);
+		assertFalse("apply edit3", edit3.undoCalled);
+		assertTrue("undo edit4", edit4.undoCalled);
+		assertTrue("undo edit5", edit5.undoCalled);
+		assertTrue("undo edit6", edit6.undoCalled);
+	}
+
+	@Test
+	public void threeEditsAppliedInOneTransaction_shouldBeUndoneWithOneCallToUndo() throws Exception {
+		final EditStack stack = new EditStack();
+		final MockEdit edit1 = new MockEdit();
+		final MockEdit edit2 = new MockEdit();
+		final MockEdit edit3 = new MockEdit();
+
+		stack.beginWork();
+		stack.apply(edit1);
+		stack.apply(edit2);
+		stack.apply(edit3);
+		stack.commitWork();
+		stack.undo();
+
+		assertTrue("undo edit1", edit1.undoCalled);
+		assertTrue("undo edit2", edit2.undoCalled);
+		assertTrue("undo edit3", edit3.undoCalled);
+	}
+
+	@Test
+	public void threeEditsCommitted_shouldProvideOffsetAfterLastEdit() throws Exception {
+		final EditStack stack = new EditStack();
+		final MockEdit lastEdit = new MockEdit();
+		lastEdit.offsetAfter = 123;
+
+		stack.beginWork();
+		stack.apply(new MockEdit());
+		stack.apply(new MockEdit());
+		stack.apply(lastEdit);
+		final IUndoableEdit committedEdit = stack.commitWork();
+
+		assertEquals(123, committedEdit.getOffsetAfter());
+	}
+
+	@Test
+	public void threeEditsRolledBack_shouldProvideOffsetBeforeFirstEdit() throws Exception {
+		final EditStack stack = new EditStack();
+		final MockEdit firstEdit = new MockEdit();
+		firstEdit.offsetBefore = 123;
+
+		stack.beginWork();
+		stack.apply(firstEdit);
+		stack.apply(new MockEdit());
+		stack.apply(new MockEdit());
+		final IUndoableEdit rolledbackEdit = stack.rollbackWork();
+
+		assertEquals(123, rolledbackEdit.getOffsetBefore());
+	}
+
+	private static class MockEdit implements IUndoableEdit {
+
+		public boolean redoCalled;
+		public boolean undoCalled;
+		public boolean combineCalled;
+		private final boolean canUndo;
+		private final boolean canRedo;
+		public boolean canCombine;
+		public int offsetBefore;
+		public int offsetAfter;
+
+		public MockEdit() {
+			this(true, true);
+		}
+
+		public MockEdit(final boolean canUndo, final boolean canRedo) {
+			this.canUndo = canUndo;
+			this.canRedo = canRedo;
+		}
+
+		@Override
+		public boolean combine(final IUndoableEdit edit) {
+			combineCalled = true;
+			return canCombine;
+		}
+
+		@Override
+		public void redo() throws CannotApplyException {
+			redoCalled = true;
+		}
+
+		@Override
+		public void undo() throws CannotUndoException {
+			undoCalled = true;
+		}
+
+		@Override
+		public boolean canUndo() {
+			return canUndo;
+		}
+
+		@Override
+		public boolean canRedo() {
+			return canRedo;
+		}
+
+		public int getOffsetBefore() {
+			return offsetBefore;
+		}
+
+		public int getOffsetAfter() {
+			return offsetAfter;
+		}
+	}
+}
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/validator/DTDValidatorTest.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/validator/DTDValidatorTest.java
index b19138d..e49ef5b 100644
--- a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/validator/DTDValidatorTest.java
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/validator/DTDValidatorTest.java
@@ -103,11 +103,11 @@
 		assertValidItemsAt(doc, 0);
 		assertValidItemsAt(doc, 1);
 		assertValidItemsAt(doc, 2, "title", "para");
-		assertValidItemsAt(doc, 3);
-		assertValidItemsAt(doc, 4);
-		assertValidItemsAt(doc, 5);
+		assertValidItemsAt(doc, 3, "#PCDATA");
+		assertValidItemsAt(doc, 4, "#PCDATA");
+		assertValidItemsAt(doc, 5, "#PCDATA");
 		assertValidItemsAt(doc, 6, "title", "para");
-		assertValidItemsAt(doc, 7, "emphasis", "pre");
+		assertValidItemsAt(doc, 7, "emphasis", "pre", "#PCDATA");
 		assertValidItemsAt(doc, 8, "title", "para");
 	}
 
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/validator/SchemaValidatorTest.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/validator/SchemaValidatorTest.java
index 7f4ce23..45a411d 100644
--- a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/validator/SchemaValidatorTest.java
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/validator/SchemaValidatorTest.java
@@ -1,307 +1,307 @@
-/*******************************************************************************

- * Copyright (c) 2011, 2013 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

- * 		Carsten Hiesserich - tests for attribute namespaces

- *******************************************************************************/

-package org.eclipse.vex.core.internal.validator;

-

-import static org.eclipse.vex.core.provisional.dom.IValidator.PCDATA;

-import static org.eclipse.vex.core.tests.TestResources.CONTENT_NS;

-import static org.eclipse.vex.core.tests.TestResources.STRUCTURE_NS;

-import static org.eclipse.vex.core.tests.TestResources.TEST_DTD;

-import static org.eclipse.vex.core.tests.TestResources.getAsStream;

-import static org.junit.Assert.assertEquals;

-import static org.junit.Assert.assertNotNull;

-import static org.junit.Assert.assertTrue;

-

-import java.io.InputStream;

-import java.util.ArrayList;

-import java.util.Collections;

-import java.util.HashMap;

-import java.util.HashSet;

-import java.util.List;

-import java.util.Map;

-import java.util.Set;

-

-import org.eclipse.core.runtime.QualifiedName;

-import org.eclipse.vex.core.internal.dom.Document;

-import org.eclipse.vex.core.internal.dom.Element;

-import org.eclipse.vex.core.internal.dom.Namespace;

-import org.eclipse.vex.core.internal.io.DocumentReader;

-import org.eclipse.vex.core.provisional.dom.AttributeDefinition;

-import org.eclipse.vex.core.provisional.dom.DocumentContentModel;

-import org.eclipse.vex.core.provisional.dom.IDocument;

-import org.eclipse.vex.core.provisional.dom.IElement;

-import org.eclipse.vex.core.provisional.dom.IValidator;

-import org.eclipse.wst.common.uriresolver.internal.provisional.URIResolver;

-import org.eclipse.wst.common.uriresolver.internal.provisional.URIResolverPlugin;

-import org.eclipse.wst.xml.core.internal.contentmodel.CMDocument;

-import org.eclipse.wst.xml.core.internal.contentmodel.CMElementDeclaration;

-import org.eclipse.wst.xml.core.internal.contentmodel.ContentModelManager;

-import org.junit.Test;

-import org.xml.sax.InputSource;

-

-/**

- * @author Florian Thienel

- */

-public class SchemaValidatorTest {

-

-	private static final QualifiedName CHAPTER = new QualifiedName(STRUCTURE_NS, "chapter");

-	private static final QualifiedName TITLE = new QualifiedName(STRUCTURE_NS, "title");

-

-	private static final QualifiedName P = new QualifiedName(CONTENT_NS, "p");

-	private static final QualifiedName B = new QualifiedName(CONTENT_NS, "b");

-	private static final QualifiedName I = new QualifiedName(CONTENT_NS, "i");

-	private static final QualifiedName XI = new QualifiedName(Namespace.XINCLUDE_NAMESPACE_URI, "include");

-

-	@Test

-	public void readDocumentWithTwoSchemas() throws Exception {

-		final InputStream documentStream = getAsStream("document.xml");

-		final InputSource documentInputSource = new InputSource(documentStream);

-

-		final DocumentReader reader = new DocumentReader();

-		reader.setDebugging(true);

-		final IDocument document = reader.read(documentInputSource);

-		assertNotNull(document);

-

-		final IElement rootElement = document.getRootElement();

-		assertNotNull(rootElement);

-		assertEquals("chapter", rootElement.getLocalName());

-		assertEquals("chapter", rootElement.getPrefixedName());

-		assertEquals(CHAPTER, rootElement.getQualifiedName());

-		assertEquals(STRUCTURE_NS, rootElement.getDefaultNamespaceURI());

-		assertEquals(CONTENT_NS, rootElement.getNamespaceURI("c"));

-

-		final IElement subChapterElement = rootElement.childElements().get(1);

-		assertEquals("chapter", subChapterElement.getPrefixedName());

-		assertEquals(CHAPTER, subChapterElement.getQualifiedName());

-

-		final IElement paragraphElement = subChapterElement.childElements().get(1);

-		assertEquals("p", paragraphElement.getLocalName());

-		assertEquals("c:p", paragraphElement.getPrefixedName());

-		assertEquals(P, paragraphElement.getQualifiedName());

-	}

-

-	@Test

-	public void getCMDocumentsByLogicalName() throws Exception {

-		final URIResolver uriResolver = URIResolverPlugin.createResolver();

-		final ContentModelManager modelManager = ContentModelManager.getInstance();

-

-		final String schemaLocation = uriResolver.resolve(null, STRUCTURE_NS, null);

-		assertNotNull(schemaLocation);

-		final CMDocument schema = modelManager.createCMDocument(schemaLocation, null);

-		assertNotNull(schema);

-

-		final String dtdLocation = uriResolver.resolve(null, TEST_DTD, null);

-		assertNotNull(dtdLocation);

-		final CMDocument dtd = modelManager.createCMDocument(dtdLocation, null);

-		assertNotNull(dtd);

-	}

-

-	@Test

-	public void useCMDocument() throws Exception {

-		final URIResolver uriResolver = URIResolverPlugin.createResolver();

-		final ContentModelManager modelManager = ContentModelManager.getInstance();

-

-		final String structureSchemaLocation = uriResolver.resolve(null, STRUCTURE_NS, null);

-		final CMDocument structureSchema = modelManager.createCMDocument(structureSchemaLocation, null);

-

-		assertEquals(1, structureSchema.getElements().getLength());

-

-		final CMElementDeclaration chapterElement = (CMElementDeclaration) structureSchema.getElements().item(0);

-		assertEquals("chapter", chapterElement.getNodeName());

-

-		assertEquals(2, chapterElement.getLocalElements().getLength());

-	}

-

-	@Test

-	public void createValidatorWithNamespaceUri() throws Exception {

-		final IValidator validator = new WTPVEXValidator(CONTENT_NS);

-		assertEquals(1, validator.getValidRootElements().size());

-		assertTrue(validator.getValidRootElements().contains(P));

-	}

-

-	@Test

-	public void createValidatorWithDTDPublicId() throws Exception {

-		final IValidator validator = new WTPVEXValidator(TEST_DTD);

-		assertEquals(11, validator.getValidRootElements().size());

-	}

-

-	@Test

-	public void validateSimpleSchema() throws Exception {

-		final IValidator validator = new WTPVEXValidator(CONTENT_NS);

-		assertIsValidSequence(validator, P, PCDATA);

-		assertIsValidSequence(validator, P, B, I);

-		assertIsValidSequence(validator, B, B, I);

-		assertIsValidSequence(validator, B, I, B);

-		assertIsValidSequence(validator, B, PCDATA, I, B);

-		assertIsValidSequence(validator, I, B, I);

-		assertIsValidSequence(validator, I, I, B);

-		assertIsValidSequence(validator, I, PCDATA, I, B);

-	}

-

-	@Test

-	public void validItemsFromSimpleSchema() throws Exception {

-		final IValidator validator = new WTPVEXValidator();

-		final IDocument doc = new Document(P);

-		doc.setValidator(validator);

-		doc.insertElement(2, B);

-		doc.insertElement(3, I);

-

-		assertValidItems(validator, doc.getRootElement(), B, I); // p

-		assertValidItems(validator, doc.getElementForInsertionAt(2), B, I); // b

-		assertValidItems(validator, doc.getElementForInsertionAt(3), B, I); // i

-	}

-

-	@Test

-	public void validateComplexSchema() throws Exception {

-		final IValidator validator = new WTPVEXValidator(STRUCTURE_NS);

-		assertIsValidSequence(validator, CHAPTER, TITLE, P);

-		assertIsValidSequence(validator, CHAPTER, P);

-		assertIsValidSequence(validator, P, PCDATA, B, I);

-	}

-

-	@Test

-	public void validItemsFromComplexSchema() throws Exception {

-		/*

-		 * We have to check this using a document, because B and I are not defined as standalone elements. The Validator

-		 * needs their parent to find the definition of their content model.

-		 */

-		final IValidator validator = new WTPVEXValidator();

-		final IDocument doc = new Document(CHAPTER);

-		doc.setValidator(validator);

-		doc.insertElement(2, TITLE);

-		doc.insertElement(4, P);

-		doc.insertElement(5, B);

-		doc.insertElement(6, I);

-

-		assertValidItems(validator, doc.getRootElement(), CHAPTER, TITLE, P); // chapter

-		assertValidItems(validator, doc.getElementForInsertionAt(3)); // title

-		assertValidItems(validator, doc.getElementForInsertionAt(5), B, I); // p

-		assertValidItems(validator, doc.getElementForInsertionAt(6), B, I); // b

-		assertValidItems(validator, doc.getElementForInsertionAt(7), B, I); // i

-	}

-

-	@Test

-	public void getAllRequiredNamespacesForSimpleSchema() throws Exception {

-		final IValidator validator = new WTPVEXValidator(new DocumentContentModel(null, null, null, new Element(P)));

-		final Set<String> requiredNamespaces = validator.getRequiredNamespaces();

-		assertEquals(1, requiredNamespaces.size());

-	}

-

-	@Test

-	public void getAllRequiredNamespacesForComplexSchema() throws Exception {

-		final IValidator validator = new WTPVEXValidator(new DocumentContentModel(null, null, null, new Element(CHAPTER)));

-		final Set<String> requiredNamespaces = validator.getRequiredNamespaces();

-		assertEquals(2, requiredNamespaces.size());

-		assertTrue(requiredNamespaces.contains(CONTENT_NS));

-		assertTrue(requiredNamespaces.contains(STRUCTURE_NS));

-	}

-

-	private void assertIsValidSequence(final IValidator validator, final QualifiedName parentElement, final QualifiedName... sequence) {

-		for (int i = 0; i < sequence.length; i++) {

-			final List<QualifiedName> prefix = createPrefix(i, sequence);

-			final List<QualifiedName> toInsert = Collections.singletonList(sequence[i]);

-			final List<QualifiedName> suffix = createSuffix(i, sequence);

-

-			assertTrue(validator.isValidSequence(parentElement, prefix, toInsert, suffix, false));

-		}

-	}

-

-	private static List<QualifiedName> createPrefix(final int index, final QualifiedName... sequence) {

-		final List<QualifiedName> prefix = new ArrayList<QualifiedName>();

-		for (int i = 0; i < index; i++) {

-			prefix.add(sequence[i]);

-		}

-		return prefix;

-	}

-

-	private static List<QualifiedName> createSuffix(final int index, final QualifiedName... sequence) {

-		final List<QualifiedName> suffix = new ArrayList<QualifiedName>();

-		for (int i = index + 1; i < sequence.length; i++) {

-			suffix.add(sequence[i]);

-		}

-		return suffix;

-	}

-

-	private static void assertValidItems(final IValidator validator, final IElement element, final QualifiedName... expectedItems) {

-		final Set<QualifiedName> expected = new HashSet<QualifiedName>(expectedItems.length);

-		for (final QualifiedName expectedItem : expectedItems) {

-			expected.add(expectedItem);

-		}

-

-		final Set<QualifiedName> candidateItems = validator.getValidItems(element);

-		assertEquals(expected, candidateItems);

-	}

-

-	@Test

-	public void testGetAttributes_shouldReturnAllAttributes() throws Exception {

-		final Map<QualifiedName, AttributeDefinition> defs = getAttributeMap();

-		assertEquals("Expect all defined attributes", 4, defs.size());

-	}

-

-	@Test

-	public void testAttribueWithNamespace() throws Exception {

-		final Map<QualifiedName, AttributeDefinition> defs = getAttributeMap();

-

-		final AttributeDefinition ad = defs.get(new QualifiedName("http://www.eclipse.org/vex/test/test_ns1", "att1"));

-		assertNotNull("AttributeDefinition 'ns1:att1' not found", ad);

-		assertEquals("Namespace of ns1:att1", "http://www.eclipse.org/vex/test/test_ns1", ad.getQualifiedName().getQualifier());

-	}

-

-	@Test

-	public void testEnumAttribute() throws Exception {

-		final Map<QualifiedName, AttributeDefinition> defs = getAttributeMap();

-

-		final AttributeDefinition ad = defs.get(new QualifiedName(null, "enatt"));

-		assertEquals("enatt", ad.getName());

-

-		final String[] enumValues = ad.getValues();

-		assertEquals(3, enumValues.length);

-	}

-

-	@Test

-	public void testDefaultValue() throws Exception {

-		final Map<QualifiedName, AttributeDefinition> defs = getAttributeMap();

-

-		final AttributeDefinition ad = defs.get(new QualifiedName(null, "enatt"));

-		assertNotNull("AttributeDefinition 'enatt' not found", ad);

-		assertEquals("Default value of 'enatt'", "value1", ad.getDefaultValue());

-	}

-

-	@Test

-	public void testRequiredAttribute() throws Exception {

-		final Map<QualifiedName, AttributeDefinition> defs = getAttributeMap();

-

-		final AttributeDefinition ad = defs.get(new QualifiedName(null, "reqatt"));

-		assertNotNull("AttributeDefinition 'reqatt' not found", ad);

-		assertTrue("isRequired should be true", ad.isRequired());

-	}

-

-	@Test

-	public void testValidationShouldIgnoreXInclude() throws Exception {

-		final IValidator validator = new WTPVEXValidator(CONTENT_NS);

-		assertIsValidSequence(validator, P, XI);

-		assertIsValidSequence(validator, P, XI, B, I);

-	}

-

-	private Map<QualifiedName, AttributeDefinition> getAttributeMap() {

-		final IDocument doc = new Document(new QualifiedName(null, "chapter"));

-		final IValidator validator = new WTPVEXValidator(STRUCTURE_NS);

-		doc.setValidator(validator);

-		final IElement chapterElement = doc.getRootElement();

-

-		final List<AttributeDefinition> atts = validator.getAttributeDefinitions(chapterElement);

-		final Map<QualifiedName, AttributeDefinition> adMap = new HashMap<QualifiedName, AttributeDefinition>();

-		for (final AttributeDefinition ad : atts) {

-			adMap.put(ad.getQualifiedName(), ad);

-		}

-		return adMap;

-	}

-}

+/*******************************************************************************
+ * Copyright (c) 2011, 2013 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
+ * 		Carsten Hiesserich - tests for attribute namespaces
+ *******************************************************************************/
+package org.eclipse.vex.core.internal.validator;
+
+import static org.eclipse.vex.core.provisional.dom.IValidator.PCDATA;
+import static org.eclipse.vex.core.tests.TestResources.CONTENT_NS;
+import static org.eclipse.vex.core.tests.TestResources.STRUCTURE_NS;
+import static org.eclipse.vex.core.tests.TestResources.TEST_DTD;
+import static org.eclipse.vex.core.tests.TestResources.getAsStream;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.core.runtime.QualifiedName;
+import org.eclipse.vex.core.internal.dom.Document;
+import org.eclipse.vex.core.internal.dom.Element;
+import org.eclipse.vex.core.internal.dom.Namespace;
+import org.eclipse.vex.core.internal.io.DocumentReader;
+import org.eclipse.vex.core.provisional.dom.AttributeDefinition;
+import org.eclipse.vex.core.provisional.dom.DocumentContentModel;
+import org.eclipse.vex.core.provisional.dom.IDocument;
+import org.eclipse.vex.core.provisional.dom.IElement;
+import org.eclipse.vex.core.provisional.dom.IValidator;
+import org.eclipse.wst.common.uriresolver.internal.provisional.URIResolver;
+import org.eclipse.wst.common.uriresolver.internal.provisional.URIResolverPlugin;
+import org.eclipse.wst.xml.core.internal.contentmodel.CMDocument;
+import org.eclipse.wst.xml.core.internal.contentmodel.CMElementDeclaration;
+import org.eclipse.wst.xml.core.internal.contentmodel.ContentModelManager;
+import org.junit.Test;
+import org.xml.sax.InputSource;
+
+/**
+ * @author Florian Thienel
+ */
+public class SchemaValidatorTest {
+
+	private static final QualifiedName CHAPTER = new QualifiedName(STRUCTURE_NS, "chapter");
+	private static final QualifiedName TITLE = new QualifiedName(STRUCTURE_NS, "title");
+
+	private static final QualifiedName P = new QualifiedName(CONTENT_NS, "p");
+	private static final QualifiedName B = new QualifiedName(CONTENT_NS, "b");
+	private static final QualifiedName I = new QualifiedName(CONTENT_NS, "i");
+	private static final QualifiedName XI = new QualifiedName(Namespace.XINCLUDE_NAMESPACE_URI, "include");
+
+	@Test
+	public void readDocumentWithTwoSchemas() throws Exception {
+		final InputStream documentStream = getAsStream("document.xml");
+		final InputSource documentInputSource = new InputSource(documentStream);
+
+		final DocumentReader reader = new DocumentReader();
+		reader.setDebugging(true);
+		final IDocument document = reader.read(documentInputSource);
+		assertNotNull(document);
+
+		final IElement rootElement = document.getRootElement();
+		assertNotNull(rootElement);
+		assertEquals("chapter", rootElement.getLocalName());
+		assertEquals("chapter", rootElement.getPrefixedName());
+		assertEquals(CHAPTER, rootElement.getQualifiedName());
+		assertEquals(STRUCTURE_NS, rootElement.getDefaultNamespaceURI());
+		assertEquals(CONTENT_NS, rootElement.getNamespaceURI("c"));
+
+		final IElement subChapterElement = rootElement.childElements().get(1);
+		assertEquals("chapter", subChapterElement.getPrefixedName());
+		assertEquals(CHAPTER, subChapterElement.getQualifiedName());
+
+		final IElement paragraphElement = subChapterElement.childElements().get(1);
+		assertEquals("p", paragraphElement.getLocalName());
+		assertEquals("c:p", paragraphElement.getPrefixedName());
+		assertEquals(P, paragraphElement.getQualifiedName());
+	}
+
+	@Test
+	public void getCMDocumentsByLogicalName() throws Exception {
+		final URIResolver uriResolver = URIResolverPlugin.createResolver();
+		final ContentModelManager modelManager = ContentModelManager.getInstance();
+
+		final String schemaLocation = uriResolver.resolve(null, STRUCTURE_NS, null);
+		assertNotNull(schemaLocation);
+		final CMDocument schema = modelManager.createCMDocument(schemaLocation, null);
+		assertNotNull(schema);
+
+		final String dtdLocation = uriResolver.resolve(null, TEST_DTD, null);
+		assertNotNull(dtdLocation);
+		final CMDocument dtd = modelManager.createCMDocument(dtdLocation, null);
+		assertNotNull(dtd);
+	}
+
+	@Test
+	public void useCMDocument() throws Exception {
+		final URIResolver uriResolver = URIResolverPlugin.createResolver();
+		final ContentModelManager modelManager = ContentModelManager.getInstance();
+
+		final String structureSchemaLocation = uriResolver.resolve(null, STRUCTURE_NS, null);
+		final CMDocument structureSchema = modelManager.createCMDocument(structureSchemaLocation, null);
+
+		assertEquals(1, structureSchema.getElements().getLength());
+
+		final CMElementDeclaration chapterElement = (CMElementDeclaration) structureSchema.getElements().item(0);
+		assertEquals("chapter", chapterElement.getNodeName());
+
+		assertEquals(2, chapterElement.getLocalElements().getLength());
+	}
+
+	@Test
+	public void createValidatorWithNamespaceUri() throws Exception {
+		final IValidator validator = new WTPVEXValidator(CONTENT_NS);
+		assertEquals(1, validator.getValidRootElements().size());
+		assertTrue(validator.getValidRootElements().contains(P));
+	}
+
+	@Test
+	public void createValidatorWithDTDPublicId() throws Exception {
+		final IValidator validator = new WTPVEXValidator(TEST_DTD);
+		assertEquals(11, validator.getValidRootElements().size());
+	}
+
+	@Test
+	public void validateSimpleSchema() throws Exception {
+		final IValidator validator = new WTPVEXValidator(CONTENT_NS);
+		assertIsValidSequence(validator, P, PCDATA);
+		assertIsValidSequence(validator, P, B, I);
+		assertIsValidSequence(validator, B, B, I);
+		assertIsValidSequence(validator, B, I, B);
+		assertIsValidSequence(validator, B, PCDATA, I, B);
+		assertIsValidSequence(validator, I, B, I);
+		assertIsValidSequence(validator, I, I, B);
+		assertIsValidSequence(validator, I, PCDATA, I, B);
+	}
+
+	@Test
+	public void validItemsFromSimpleSchema() throws Exception {
+		final IValidator validator = new WTPVEXValidator();
+		final IDocument doc = new Document(P);
+		doc.setValidator(validator);
+		doc.insertElement(2, B);
+		doc.insertElement(3, I);
+
+		assertValidItems(validator, doc.getRootElement(), B, I, PCDATA); // p
+		assertValidItems(validator, doc.getElementForInsertionAt(2), B, I, PCDATA); // b
+		assertValidItems(validator, doc.getElementForInsertionAt(3), B, I, PCDATA); // i
+	}
+
+	@Test
+	public void validateComplexSchema() throws Exception {
+		final IValidator validator = new WTPVEXValidator(STRUCTURE_NS);
+		assertIsValidSequence(validator, CHAPTER, TITLE, P);
+		assertIsValidSequence(validator, CHAPTER, P);
+		assertIsValidSequence(validator, P, PCDATA, B, I);
+	}
+
+	@Test
+	public void validItemsFromComplexSchema() throws Exception {
+		/*
+		 * We have to check this using a document, because B and I are not defined as standalone elements. The Validator
+		 * needs their parent to find the definition of their content model.
+		 */
+		final IValidator validator = new WTPVEXValidator();
+		final IDocument doc = new Document(CHAPTER);
+		doc.setValidator(validator);
+		doc.insertElement(2, TITLE);
+		doc.insertElement(4, P);
+		doc.insertElement(5, B);
+		doc.insertElement(6, I);
+
+		assertValidItems(validator, doc.getRootElement(), CHAPTER, TITLE, P); // chapter
+		assertValidItems(validator, doc.getElementForInsertionAt(3), PCDATA); // title
+		assertValidItems(validator, doc.getElementForInsertionAt(5), B, I, PCDATA); // p
+		assertValidItems(validator, doc.getElementForInsertionAt(6), B, I, PCDATA); // b
+		assertValidItems(validator, doc.getElementForInsertionAt(7), B, I, PCDATA); // i
+	}
+
+	@Test
+	public void getAllRequiredNamespacesForSimpleSchema() throws Exception {
+		final IValidator validator = new WTPVEXValidator(new DocumentContentModel(null, null, null, new Element(P)));
+		final Set<String> requiredNamespaces = validator.getRequiredNamespaces();
+		assertEquals(1, requiredNamespaces.size());
+	}
+
+	@Test
+	public void getAllRequiredNamespacesForComplexSchema() throws Exception {
+		final IValidator validator = new WTPVEXValidator(new DocumentContentModel(null, null, null, new Element(CHAPTER)));
+		final Set<String> requiredNamespaces = validator.getRequiredNamespaces();
+		assertEquals(2, requiredNamespaces.size());
+		assertTrue(requiredNamespaces.contains(CONTENT_NS));
+		assertTrue(requiredNamespaces.contains(STRUCTURE_NS));
+	}
+
+	private void assertIsValidSequence(final IValidator validator, final QualifiedName parentElement, final QualifiedName... sequence) {
+		for (int i = 0; i < sequence.length; i++) {
+			final List<QualifiedName> prefix = createPrefix(i, sequence);
+			final List<QualifiedName> toInsert = Collections.singletonList(sequence[i]);
+			final List<QualifiedName> suffix = createSuffix(i, sequence);
+
+			assertTrue(validator.isValidSequence(parentElement, prefix, toInsert, suffix, false));
+		}
+	}
+
+	private static List<QualifiedName> createPrefix(final int index, final QualifiedName... sequence) {
+		final List<QualifiedName> prefix = new ArrayList<QualifiedName>();
+		for (int i = 0; i < index; i++) {
+			prefix.add(sequence[i]);
+		}
+		return prefix;
+	}
+
+	private static List<QualifiedName> createSuffix(final int index, final QualifiedName... sequence) {
+		final List<QualifiedName> suffix = new ArrayList<QualifiedName>();
+		for (int i = index + 1; i < sequence.length; i++) {
+			suffix.add(sequence[i]);
+		}
+		return suffix;
+	}
+
+	private static void assertValidItems(final IValidator validator, final IElement element, final QualifiedName... expectedItems) {
+		final Set<QualifiedName> expected = new HashSet<QualifiedName>(expectedItems.length);
+		for (final QualifiedName expectedItem : expectedItems) {
+			expected.add(expectedItem);
+		}
+
+		final Set<QualifiedName> candidateItems = validator.getValidItems(element);
+		assertEquals(expected, candidateItems);
+	}
+
+	@Test
+	public void testGetAttributes_shouldReturnAllAttributes() throws Exception {
+		final Map<QualifiedName, AttributeDefinition> defs = getAttributeMap();
+		assertEquals("Expect all defined attributes", 4, defs.size());
+	}
+
+	@Test
+	public void testAttribueWithNamespace() throws Exception {
+		final Map<QualifiedName, AttributeDefinition> defs = getAttributeMap();
+
+		final AttributeDefinition ad = defs.get(new QualifiedName("http://www.eclipse.org/vex/test/test_ns1", "att1"));
+		assertNotNull("AttributeDefinition 'ns1:att1' not found", ad);
+		assertEquals("Namespace of ns1:att1", "http://www.eclipse.org/vex/test/test_ns1", ad.getQualifiedName().getQualifier());
+	}
+
+	@Test
+	public void testEnumAttribute() throws Exception {
+		final Map<QualifiedName, AttributeDefinition> defs = getAttributeMap();
+
+		final AttributeDefinition ad = defs.get(new QualifiedName(null, "enatt"));
+		assertEquals("enatt", ad.getName());
+
+		final String[] enumValues = ad.getValues();
+		assertEquals(3, enumValues.length);
+	}
+
+	@Test
+	public void testDefaultValue() throws Exception {
+		final Map<QualifiedName, AttributeDefinition> defs = getAttributeMap();
+
+		final AttributeDefinition ad = defs.get(new QualifiedName(null, "enatt"));
+		assertNotNull("AttributeDefinition 'enatt' not found", ad);
+		assertEquals("Default value of 'enatt'", "value1", ad.getDefaultValue());
+	}
+
+	@Test
+	public void testRequiredAttribute() throws Exception {
+		final Map<QualifiedName, AttributeDefinition> defs = getAttributeMap();
+
+		final AttributeDefinition ad = defs.get(new QualifiedName(null, "reqatt"));
+		assertNotNull("AttributeDefinition 'reqatt' not found", ad);
+		assertTrue("isRequired should be true", ad.isRequired());
+	}
+
+	@Test
+	public void testValidationShouldIgnoreXInclude() throws Exception {
+		final IValidator validator = new WTPVEXValidator(CONTENT_NS);
+		assertIsValidSequence(validator, P, XI);
+		assertIsValidSequence(validator, P, XI, B, I);
+	}
+
+	private Map<QualifiedName, AttributeDefinition> getAttributeMap() {
+		final IDocument doc = new Document(new QualifiedName(null, "chapter"));
+		final IValidator validator = new WTPVEXValidator(STRUCTURE_NS);
+		doc.setValidator(validator);
+		final IElement chapterElement = doc.getRootElement();
+
+		final List<AttributeDefinition> atts = validator.getAttributeDefinitions(chapterElement);
+		final Map<QualifiedName, AttributeDefinition> adMap = new HashMap<QualifiedName, AttributeDefinition>();
+		for (final AttributeDefinition ad : atts) {
+			adMap.put(ad.getQualifiedName(), ad);
+		}
+		return adMap;
+	}
+}
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/BalancingSelectorTest.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/BalancingSelectorTest.java
new file mode 100644
index 0000000..fafe0b3
--- /dev/null
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/BalancingSelectorTest.java
@@ -0,0 +1,138 @@
+/*******************************************************************************
+ * 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
+ *      Carsten Hiesserich - moved additional tests from L2SelectionTest
+ *******************************************************************************/
+package org.eclipse.vex.core.internal.widget;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import org.eclipse.vex.core.internal.io.UniversalTestDocument;
+import org.eclipse.vex.core.provisional.dom.ContentRange;
+import org.eclipse.vex.core.provisional.dom.IElement;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author Florian Thienel
+ */
+public class BalancingSelectorTest {
+
+	private BalancingSelector selector;
+	private UniversalTestDocument document;
+
+	@Before
+	public void setUp() throws Exception {
+		document = new UniversalTestDocument(3);
+		selector = new BalancingSelector();
+		selector.setDocument(document.getDocument());
+	}
+
+	@Test
+	public void givenMarkInText_whenSelectingOneCharForward_shouldIncludeNextChar() throws Exception {
+		final int mark = document.getOffsetWithinText(0);
+		select(mark, mark + 1);
+		assertBalancedSelectionIs(mark, mark + 1, mark + 1);
+	}
+
+	@Test
+	public void givenMarkInText_whenSelectingOneCharBackward_shouldIncludePreviousChar() throws Exception {
+		final int mark = document.getOffsetWithinText(0);
+		select(mark, mark - 1);
+		assertBalancedSelectionIs(mark - 1, mark, mark - 1);
+	}
+
+	@Test
+	public void givenMarkAtFirstTextPosition_whenSelectingOneCharBackward_shouldSelectWholeParagraph() throws Exception {
+		final IElement paragraph = document.getParagraphWithText(0);
+		select(paragraph.getStartOffset() + 1, paragraph.getStartOffset());
+		assertBalancedSelectionIs(paragraph.getStartOffset(), paragraph.getEndOffset() + 1, paragraph.getStartOffset());
+	}
+
+	@Test
+	public void givenMarkAtLastTextPosition_whenSelectingOneCharForward_shouldSelectWholeParagraph() throws Exception {
+		final IElement paragraph = document.getParagraphWithText(0);
+		select(paragraph.getEndOffset(), paragraph.getEndOffset() + 1);
+		assertBalancedSelectionIs(paragraph.getStartOffset(), paragraph.getEndOffset() + 1, paragraph.getEndOffset() + 1);
+	}
+
+	@Test
+	public void givenMarkAtStartOffsetOfEmptyParagraph_whenSelectingOneForward_shouldSelectWholeParagraph() throws Exception {
+		final IElement paragraph = document.getEmptyParagraph(0);
+		select(paragraph.getStartOffset(), paragraph.getEndOffset());
+		assertBalancedSelectionIs(paragraph.getStartOffset(), paragraph.getEndOffset() + 1, paragraph.getEndOffset() + 1);
+	}
+
+	@Test
+	public void givenMarkInText_whenSelectingToStartOffsetOfSection_shouldSelectWholeSection() throws Exception {
+		final IElement section = document.getSection(1);
+		final int mark = document.getOffsetWithinText(1);
+		select(mark, section.getStartOffset());
+		assertBalancedSelectionIs(section.getStartOffset(), section.getEndOffset() + 1, section.getStartOffset());
+	}
+
+	@Test
+	public void givenMarkInText_whenSelectingToEndOffsetOfNextParagraphMultipleTimes_shouldStayAtEndOfNextContainingSection() throws Exception {
+		final IElement section = document.getSection(0);
+		final IElement firstParagraph = document.getParagraphWithText(0);
+		final IElement secondParagraph = document.getEmptyParagraph(0);
+		select(firstParagraph.getStartOffset() + 4, secondParagraph.getEndOffset());
+		assertBalancedSelectionIs(firstParagraph.getStartOffset(), section.getEndOffset(), section.getEndOffset());
+		selector.setEndAbsoluteTo(secondParagraph.getEndOffset());
+		assertBalancedSelectionIs(firstParagraph.getStartOffset(), section.getEndOffset(), section.getEndOffset());
+	}
+
+	@Test
+	public void givenMarkAtEndOfFourthParagraph_whenSelectingFourthAndThirdParagraph_shouldBeAbleToMoveToStartOfSecondSection() throws Exception {
+		final IElement section = document.getSection(1);
+		final IElement thirdParagraph = document.getParagraphWithText(1);
+		final IElement fourthParagraph = document.getEmptyParagraph(1);
+		select(fourthParagraph.getEndOffset(), thirdParagraph.getEndOffset());
+		assertBalancedSelectionIs(thirdParagraph.getStartOffset(), fourthParagraph.getEndOffset() + 1, thirdParagraph.getStartOffset());
+		selector.moveEndTo(thirdParagraph.getStartOffset() - 1);
+		assertBalancedSelectionIs(section.getStartOffset(), section.getEndOffset() + 1, section.getStartOffset());
+	}
+
+	@Test
+	public void givenMarkAtStartOffsetOfParagraphWithText_whenSelectingForward_shouldSelectWholeParagraph() throws Exception {
+		final IElement paragraphWithText = document.getParagraphWithText(0);
+		select(paragraphWithText.getStartOffset(), paragraphWithText.getStartOffset() + 1);
+		assertBalancedSelectionIs(paragraphWithText.getStartOffset(), paragraphWithText.getEndOffset() + 1, paragraphWithText.getEndOffset() + 1);
+	}
+
+	@Test
+	public void givenMarkAtEndOffsetOfParagraphWithText_whenSelectingBackwardOneCharBehindStartOffset_shouldNotIncludeEndOffsetInSelectedRange() throws Exception {
+		final IElement paragraphWithText = document.getParagraphWithText(0);
+		select(paragraphWithText.getEndOffset(), paragraphWithText.getStartOffset() + 1);
+		assertBalancedSelectionIs(paragraphWithText.getStartOffset() + 1, paragraphWithText.getEndOffset(), paragraphWithText.getStartOffset() + 1);
+	}
+
+	@Test
+	public void givenMarkAtStartOffsetOfParagraphWithText_whenSelectingOneForwardAndOneBackward_shouldSelectNothing() throws Exception {
+		final IElement paragraphWithText = document.getParagraphWithText(0);
+		select(paragraphWithText.getStartOffset(), paragraphWithText.getStartOffset() + 1);
+		selector.moveEndTo(paragraphWithText.getStartOffset());
+		assertFalse(selector.isActive());
+	}
+
+	/*
+	 * Utility Methods
+	 */
+
+	private void select(final int mark, final int caretPosition) {
+		selector.setMark(mark);
+		selector.moveEndTo(caretPosition);
+	}
+
+	private void assertBalancedSelectionIs(final int startOffset, final int endOffset, final int caretOffset) {
+		assertEquals("selection", new ContentRange(startOffset, endOffset), selector.getRange());
+		assertEquals("caret offset", caretOffset, selector.getCaretOffset());
+	}
+}
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/BaseClipboardTest.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/BaseClipboardTest.java
new file mode 100644
index 0000000..dfb8ae5
--- /dev/null
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/BaseClipboardTest.java
@@ -0,0 +1,176 @@
+/*******************************************************************************
+ * Copyright (c) 2016 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.widget;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import org.eclipse.vex.core.internal.io.UniversalTestDocument;
+import org.eclipse.vex.core.provisional.dom.IElement;
+import org.eclipse.vex.core.provisional.dom.INode;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public abstract class BaseClipboardTest {
+
+	private UniversalTestDocument document;
+	private DocumentEditor editor;
+	private IClipboard clipboard;
+
+	protected abstract IClipboard createClipboard();
+
+	@Before
+	public void setUp() throws Exception {
+		document = new UniversalTestDocument(1);
+		editor = new DocumentEditor(new FakeCursor(document.getDocument()));
+		editor.setDocument(document.getDocument());
+		clipboard = createClipboard();
+	}
+
+	@After
+	public void disposeClipboard() {
+		clipboard.dispose();
+	}
+
+	@Test
+	public void givenFirstParagraphSelected_cutSelection_shouldRemoveFirstParagraphFromDocument() throws Exception {
+		final IElement section = document.getSection(0);
+		final INode firstParagraph = section.children().first();
+
+		editor.select(firstParagraph);
+		clipboard.cutSelection(editor);
+
+		assertNotSame(firstParagraph, section.children().first());
+		assertNull(firstParagraph.getDocument());
+	}
+
+	@Test
+	public void givenFirstParagraphSelected_cutAndPasteAfterSecondParagraph_shouldInsertFirstParagraphAfterSecondParagraph() throws Exception {
+		final IElement section = document.getSection(0);
+		final INode firstParagraph = section.children().first();
+		final INode secondParagraph = section.children().last();
+		final String firstParagraphContent = firstParagraph.getText();
+
+		editor.select(firstParagraph);
+		clipboard.cutSelection(editor);
+		editor.moveTo(secondParagraph.getEndPosition().moveBy(1));
+		clipboard.paste(editor);
+
+		assertEquals(firstParagraphContent, section.children().last().getText());
+		assertEquals(2, section.children().count());
+	}
+
+	@Test
+	public void givenFirstParagraphSelected_copyAndPasteAfterSecondParagraph_shouldInsertFirstParagraphAgainAfterSecondParagraph() throws Exception {
+		final IElement section = document.getSection(0);
+		final INode firstParagraph = section.children().first();
+		final INode secondParagraph = section.children().last();
+		final String firstParagraphContent = firstParagraph.getText();
+
+		editor.select(firstParagraph);
+		clipboard.copySelection(editor);
+		editor.moveTo(secondParagraph.getEndPosition().moveBy(1));
+		clipboard.paste(editor);
+
+		assertEquals(firstParagraphContent, section.children().last().getText());
+		assertEquals(3, section.children().count());
+	}
+
+	@Test
+	public void givenNothingSelected_cutSelection_shouldNotChangeClipboardContent() throws Exception {
+		final IElement section = document.getSection(0);
+		final INode firstParagraph = section.children().first();
+		final INode secondParagraph = section.children().last();
+		final String firstParagraphContent = firstParagraph.getText();
+
+		editor.select(firstParagraph);
+		clipboard.copySelection(editor);
+		editor.moveTo(secondParagraph.getEndPosition().moveBy(1));
+		clipboard.cutSelection(editor);
+		clipboard.paste(editor);
+
+		assertEquals(firstParagraphContent, section.children().last().getText());
+		assertEquals(3, section.children().count());
+	}
+
+	@Test
+	public void givenNothingSelected_copySelection_shouldNotChangeClipboardContent() throws Exception {
+		final IElement section = document.getSection(0);
+		final INode firstParagraph = section.children().first();
+		final INode secondParagraph = section.children().last();
+		final String firstParagraphContent = firstParagraph.getText();
+
+		editor.select(firstParagraph);
+		clipboard.copySelection(editor);
+		editor.moveTo(secondParagraph.getEndPosition().moveBy(1));
+		clipboard.copySelection(editor);
+		clipboard.paste(editor);
+
+		assertEquals(firstParagraphContent, section.children().last().getText());
+		assertEquals(3, section.children().count());
+	}
+
+	@Test
+	public void givenFirstParagraphSelected_copyAndPasteTextIntoSecondParagraph_shouldInsertTextOfFirstParagraphIntoSecondParagraph() throws Exception {
+		final IElement section = document.getSection(0);
+		final INode firstParagraph = section.children().first();
+		final INode secondParagraph = section.children().last();
+		final String firstParagraphContent = firstParagraph.getText();
+
+		editor.select(firstParagraph);
+		clipboard.copySelection(editor);
+		editor.moveTo(secondParagraph.getEndPosition());
+		clipboard.pasteText(editor);
+
+		assertEquals(firstParagraphContent, section.children().last().getText());
+		assertEquals(2, section.children().count());
+	}
+
+	@Test
+	public void givenFirstParagraphSelected_copySelection_shouldIndicateContentAndTextContentAvailable() throws Exception {
+		final IElement section = document.getSection(0);
+		final INode firstParagraph = section.children().first();
+
+		editor.select(firstParagraph);
+		clipboard.copySelection(editor);
+
+		assertTrue("hasContent", clipboard.hasContent());
+		assertTrue("hasTextContent", clipboard.hasTextContent());
+	}
+
+	@Test
+	public void givenContentOfFirstParagraphSelected_copySelection_shouldIndicateContentAndTextContentAvailable() throws Exception {
+		final IElement section = document.getSection(0);
+		final INode firstParagraph = section.children().first();
+
+		editor.selectContentOf(firstParagraph);
+		clipboard.copySelection(editor);
+
+		assertTrue("hasContent", clipboard.hasContent());
+		assertTrue("hasTextContent", clipboard.hasTextContent());
+	}
+
+	@Test
+	public void givenSecondParagraphSelected_copySelection_shouldIndicateContentButNotTextContentAvailable() throws Exception {
+		final IElement section = document.getSection(0);
+		final INode secondParagraph = section.children().last();
+
+		editor.select(secondParagraph);
+		clipboard.copySelection(editor);
+
+		assertTrue("hasContent", clipboard.hasContent());
+		assertFalse("hasTextContent", clipboard.hasTextContent());
+	}
+}
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/FakeCursor.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/FakeCursor.java
new file mode 100644
index 0000000..2bd9ffa
--- /dev/null
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/FakeCursor.java
@@ -0,0 +1,111 @@
+/*******************************************************************************
+ * Copyright (c) 2016 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.widget;
+
+import java.util.LinkedList;
+
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.cursor.ContentTopology;
+import org.eclipse.vex.core.internal.cursor.ICursor;
+import org.eclipse.vex.core.internal.cursor.ICursorMove;
+import org.eclipse.vex.core.internal.cursor.ICursorPositionListener;
+import org.eclipse.vex.core.internal.layout.FakeGraphics;
+import org.eclipse.vex.core.provisional.dom.ContentRange;
+import org.eclipse.vex.core.provisional.dom.IDocument;
+
+public class FakeCursor implements ICursor {
+
+	private final LinkedList<ICursorPositionListener> cursorPositionListeners = new LinkedList<ICursorPositionListener>();
+	private final Graphics graphics = new FakeGraphics();
+	private final ContentTopology contentTopology;
+	private final BalancingSelector selector;
+	private final IViewPort viewPort = new FakeViewPort();
+
+	private IDocument document;
+
+	public FakeCursor(final IDocument document) {
+		this.document = document;
+		contentTopology = new ContentTopology() {
+			@Override
+			public int getLastOffset() {
+				return FakeCursor.this.document.getEndOffset();
+			}
+		};
+		selector = new BalancingSelector();
+		selector.setDocument(document);
+	}
+
+	public void setDocument(final IDocument document) {
+		this.document = document;
+		selector.setDocument(document);
+	}
+
+	@Override
+	public int getOffset() {
+		return selector.getCaretOffset();
+	}
+
+	@Override
+	public boolean hasSelection() {
+		return selector.isActive();
+	}
+
+	@Override
+	public ContentRange getSelectedRange() {
+		if (selector.isActive()) {
+			return selector.getRange();
+		} else {
+			return new ContentRange(selector.getCaretOffset(), selector.getCaretOffset());
+		}
+	}
+
+	@Override
+	public void addPositionListener(final ICursorPositionListener listener) {
+		cursorPositionListeners.add(listener);
+	}
+
+	@Override
+	public void removePositionListener(final ICursorPositionListener listener) {
+		cursorPositionListeners.remove(listener);
+	}
+
+	private void firePositionChanged(final int offset) {
+		for (final ICursorPositionListener listener : cursorPositionListeners) {
+			listener.positionChanged(offset);
+		}
+	}
+
+	private void firePositionAboutToChange() {
+		for (final ICursorPositionListener listener : cursorPositionListeners) {
+			listener.positionAboutToChange();
+		}
+	}
+
+	@Override
+	public void move(final ICursorMove move) {
+		firePositionAboutToChange();
+		selector.setMark(move.calculateNewOffset(graphics, viewPort, contentTopology, selector.getCaretOffset(), null, null, 0));
+		firePositionChanged(selector.getCaretOffset());
+	}
+
+	@Override
+	public void select(final ICursorMove move) {
+		firePositionAboutToChange();
+		final int newOffset = move.calculateNewOffset(graphics, viewPort, contentTopology, selector.getCaretOffset(), null, null, 0);
+		if (move.isAbsolute()) {
+			selector.setEndAbsoluteTo(newOffset);
+		} else {
+			selector.moveEndTo(newOffset);
+		}
+		firePositionChanged(selector.getCaretOffset());
+	}
+
+}
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/FakeViewPort.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/FakeViewPort.java
new file mode 100644
index 0000000..124e6e6
--- /dev/null
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/FakeViewPort.java
@@ -0,0 +1,31 @@
+/*******************************************************************************
+ * Copyright (c) 2016 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.widget;
+
+import org.eclipse.vex.core.internal.core.Rectangle;
+
+public class FakeViewPort implements IViewPort {
+
+	@Override
+	public void reconcile(final int maximumHeight) {
+		// ignore
+	}
+
+	@Override
+	public Rectangle getVisibleArea() {
+		return new Rectangle(0, 0, 100, 100);
+	}
+
+	@Override
+	public void moveRelative(final int delta) {
+		// ignore
+	}
+}
\ No newline at end of file
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/InMemoryClipboardTest.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/InMemoryClipboardTest.java
new file mode 100644
index 0000000..5b21a74
--- /dev/null
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/InMemoryClipboardTest.java
@@ -0,0 +1,20 @@
+/*******************************************************************************
+ * Copyright (c) 2016 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.widget;
+
+public class InMemoryClipboardTest extends BaseClipboardTest {
+
+	@Override
+	protected IClipboard createClipboard() {
+		return new InMemoryClipboard();
+	}
+
+}
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/L2CommentEditingTest.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/L2CommentEditingTest.java
index 5e9339d..62a056e 100644
--- a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/L2CommentEditingTest.java
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/L2CommentEditingTest.java
@@ -1,138 +1,139 @@
-/*******************************************************************************

- * Copyright (c) 2013 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.widget;

-

-import static org.eclipse.vex.core.internal.widget.VexWidgetTest.PARA;

-import static org.eclipse.vex.core.internal.widget.VexWidgetTest.TITLE;

-import static org.eclipse.vex.core.internal.widget.VexWidgetTest.createDocumentWithDTD;

-import static org.eclipse.vex.core.internal.widget.VexWidgetTest.getContentStructure;

-import static org.eclipse.vex.core.internal.widget.VexWidgetTest.getCurrentXML;

-import static org.eclipse.vex.core.tests.TestResources.TEST_DTD;

-import static org.junit.Assert.assertEquals;

-import static org.junit.Assert.assertFalse;

-import static org.junit.Assert.assertNull;

-import static org.junit.Assert.assertSame;

-import static org.junit.Assert.assertTrue;

-

-import org.eclipse.vex.core.internal.css.StyleSheet;

-import org.eclipse.vex.core.provisional.dom.IComment;

-import org.eclipse.vex.core.provisional.dom.IDocumentFragment;

-import org.eclipse.vex.core.provisional.dom.IElement;

-import org.eclipse.vex.core.provisional.dom.INode;

-import org.junit.Before;

-import org.junit.Test;

-

-/**

- * @author Florian Thienel

- */

-public class L2CommentEditingTest {

-

-	private IVexWidget widget;

-	private IElement rootElement;

-

-	@Before

-	public void setUp() throws Exception {

-		widget = new BaseVexWidget(new MockHostComponent());

-		widget.setDocument(createDocumentWithDTD(TEST_DTD, "section"), StyleSheet.NULL);

-		rootElement = widget.getDocument().getRootElement();

-	}

-

-	@Test

-	public void givenAnElement_whenInsertingAComment_elementShouldContainComment() throws Exception {

-		final IComment comment = widget.insertComment();

-		assertTrue(rootElement.getRange().contains(comment.getRange()));

-		assertSame(rootElement, comment.getParent());

-		assertEquals(comment.getEndPosition(), widget.getCaretPosition());

-	}

-

-	@Test

-	public void givenAnElementWithComment_whenInsertingTextWithinComment_shouldAddTextToComment() throws Exception {

-		final IComment comment = widget.insertComment();

-		widget.insertText("Hello World");

-		assertEquals("Hello World", comment.getText());

-	}

-

-	@Test

-	public void givenAnEmptyComment_whenCaretInCommentAndHittingBackspace_shouldDeleteComment() throws Exception {

-		final IElement titleElement = widget.insertElement(TITLE);

-		final IComment comment = widget.insertComment();

-		widget.deletePreviousChar();

-		assertFalse(titleElement.hasChildren());

-		assertFalse(comment.isAssociated());

-		assertNull(comment.getParent());

-	}

-

-	@Test

-	public void givenAnEmptyComment_whenCaretInCommentAndHittingDelete_shouldDeleteComment() throws Exception {

-		final IElement titleElement = widget.insertElement(TITLE);

-		final IComment comment = widget.insertComment();

-		widget.deleteNextChar();

-		assertFalse(titleElement.hasChildren());

-		assertFalse(comment.isAssociated());

-		assertNull(comment.getParent());

-	}

-

-	@Test

-	public void givenAComment_whenCaretInComment_shouldNotAllowToInsertAComment() throws Exception {

-		widget.insertComment();

-		assertFalse("can insert comment within comment", widget.canInsertComment());

-	}

-

-	@Test

-	public void undoRemoveCommentTag() throws Exception {

-		widget.insertElement(TITLE);

-		widget.insertText("1text before comment1");

-		widget.insertComment();

-		final INode comment = widget.getDocument().getChildAt(widget.getCaretPosition());

-		widget.insertText("2comment text2");

-		widget.moveBy(1);

-		widget.insertText("3text after comment3");

-

-		final String expectedContentStructure = getContentStructure(widget.getDocument().getRootElement());

-

-		widget.doWork(new Runnable() {

-			@Override

-			public void run() {

-				widget.moveTo(comment.getStartPosition().moveBy(1), false);

-				widget.moveTo(comment.getEndPosition().moveBy(-1), true);

-				final IDocumentFragment fragment = widget.getSelectedFragment();

-				widget.deleteSelection();

-

-				widget.moveBy(-1, false);

-				widget.moveBy(1, true);

-				widget.deleteSelection();

-

-				widget.insertFragment(fragment);

-			}

-		});

-

-		widget.undo();

-

-		assertEquals(expectedContentStructure, getContentStructure(widget.getDocument().getRootElement()));

-	}

-

-	@Test

-	public void undoRedoInsertCommentWithSubsequentDelete() throws Exception {

-		widget.insertElement(PARA);

-		final String expectedXml = getCurrentXML(widget);

-

-		final IComment comment = widget.insertComment();

-		widget.moveTo(comment.getStartPosition());

-		widget.moveTo(comment.getEndPosition(), true);

-		widget.deleteSelection();

-

-		widget.undo(); // delete

-		widget.undo(); // insert comment

-

-		assertEquals(expectedXml, getCurrentXML(widget));

-	}

-

-}

+/*******************************************************************************
+ * Copyright (c) 2013 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.widget;
+
+import static org.eclipse.vex.core.internal.widget.VexWidgetTest.PARA;
+import static org.eclipse.vex.core.internal.widget.VexWidgetTest.TITLE;
+import static org.eclipse.vex.core.internal.widget.VexWidgetTest.createDocumentWithDTD;
+import static org.eclipse.vex.core.internal.widget.VexWidgetTest.getContentStructure;
+import static org.eclipse.vex.core.internal.widget.VexWidgetTest.getCurrentXML;
+import static org.eclipse.vex.core.tests.TestResources.TEST_DTD;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import org.eclipse.vex.core.provisional.dom.IComment;
+import org.eclipse.vex.core.provisional.dom.IDocument;
+import org.eclipse.vex.core.provisional.dom.IDocumentFragment;
+import org.eclipse.vex.core.provisional.dom.IElement;
+import org.eclipse.vex.core.provisional.dom.INode;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author Florian Thienel
+ */
+public class L2CommentEditingTest {
+
+	private IDocumentEditor editor;
+	private IElement rootElement;
+
+	@Before
+	public void setUp() throws Exception {
+		final IDocument document = createDocumentWithDTD(TEST_DTD, "section");
+		editor = new DocumentEditor(new FakeCursor(document));
+		editor.setDocument(document);
+		rootElement = editor.getDocument().getRootElement();
+	}
+
+	@Test
+	public void givenAnElement_whenInsertingAComment_elementShouldContainComment() throws Exception {
+		final IComment comment = editor.insertComment();
+		assertTrue(rootElement.getRange().contains(comment.getRange()));
+		assertSame(rootElement, comment.getParent());
+		assertEquals(comment.getEndPosition(), editor.getCaretPosition());
+	}
+
+	@Test
+	public void givenAnElementWithComment_whenInsertingTextWithinComment_shouldAddTextToComment() throws Exception {
+		final IComment comment = editor.insertComment();
+		editor.insertText("Hello World");
+		assertEquals("Hello World", comment.getText());
+	}
+
+	@Test
+	public void givenAnEmptyComment_whenCaretInCommentAndHittingBackspace_shouldDeleteComment() throws Exception {
+		final IElement titleElement = editor.insertElement(TITLE);
+		final IComment comment = editor.insertComment();
+		editor.deleteBackward();
+		assertFalse(titleElement.hasChildren());
+		assertFalse(comment.isAssociated());
+		assertNull(comment.getParent());
+	}
+
+	@Test
+	public void givenAnEmptyComment_whenCaretInCommentAndHittingDelete_shouldDeleteComment() throws Exception {
+		final IElement titleElement = editor.insertElement(TITLE);
+		final IComment comment = editor.insertComment();
+		editor.deleteForward();
+		assertFalse(titleElement.hasChildren());
+		assertFalse(comment.isAssociated());
+		assertNull(comment.getParent());
+	}
+
+	@Test
+	public void givenAComment_whenCaretInComment_shouldNotAllowToInsertAComment() throws Exception {
+		editor.insertComment();
+		assertFalse("can insert comment within comment", editor.canInsertComment());
+	}
+
+	@Test
+	public void undoRemoveCommentTag() throws Exception {
+		editor.insertElement(TITLE);
+		editor.insertText("1text before comment1");
+		editor.insertComment();
+		final INode comment = editor.getDocument().getChildAt(editor.getCaretPosition());
+		editor.insertText("2comment text2");
+		editor.moveBy(1);
+		editor.insertText("3text after comment3");
+
+		final String expectedContentStructure = getContentStructure(editor.getDocument().getRootElement());
+
+		editor.doWork(new Runnable() {
+			@Override
+			public void run() {
+				editor.moveTo(comment.getStartPosition().moveBy(1), false);
+				editor.moveTo(comment.getEndPosition().moveBy(-1), true);
+				final IDocumentFragment fragment = editor.getSelectedFragment();
+				editor.deleteSelection();
+
+				editor.moveBy(-1, false);
+				editor.moveBy(1, true);
+				editor.deleteSelection();
+
+				editor.insertFragment(fragment);
+			}
+		});
+
+		editor.undo();
+
+		assertEquals(expectedContentStructure, getContentStructure(editor.getDocument().getRootElement()));
+	}
+
+	@Test
+	public void undoRedoInsertCommentWithSubsequentDelete() throws Exception {
+		editor.insertElement(PARA);
+		final String expectedXml = getCurrentXML(editor);
+
+		final IComment comment = editor.insertComment();
+		editor.moveTo(comment.getStartPosition());
+		editor.moveTo(comment.getEndPosition(), true);
+		editor.deleteSelection();
+
+		editor.undo(); // delete
+		editor.undo(); // insert comment
+
+		assertEquals(expectedXml, getCurrentXML(editor));
+	}
+
+}
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/L2DocumentPositionTest.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/L2DocumentPositionTest.java
deleted file mode 100644
index 805ebec..0000000
--- a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/L2DocumentPositionTest.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2015 Carsten Hiesserich 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:
- * 		Carsten Hiesserich - initial API and implementation
- *******************************************************************************/
-package org.eclipse.vex.core.internal.widget;
-
-import static org.eclipse.vex.core.internal.widget.VexWidgetTest.PARA;
-import static org.eclipse.vex.core.internal.widget.VexWidgetTest.TITLE;
-import static org.eclipse.vex.core.internal.widget.VexWidgetTest.createDocumentWithDTD;
-import static org.eclipse.vex.core.tests.TestResources.TEST_DTD;
-import static org.junit.Assert.assertEquals;
-
-import org.eclipse.vex.core.internal.css.StyleSheet;
-import org.eclipse.vex.core.provisional.dom.IElement;
-import org.junit.Before;
-import org.junit.Test;
-
-public class L2DocumentPositionTest {
-
-	private IVexWidget widget;
-	private MockHostComponent hostComponent;
-
-	@Before
-	public void setUp() throws Exception {
-		hostComponent = new MockHostComponent();
-		widget = new BaseVexWidget(hostComponent);
-		widget.setDocument(createDocumentWithDTD(TEST_DTD, "section"), StyleSheet.NULL);
-	}
-
-	@Test
-	public void moveToNextWord() throws Exception {
-		widget.insertElement(TITLE);
-		widget.moveBy(1);
-		final IElement paraElement = widget.insertElement(PARA);
-		widget.insertText("Test Word Another Word");
-		widget.moveTo(paraElement.getStartPosition().moveBy(1));
-		widget.moveToNextWord(false);
-		assertEquals(paraElement.getStartPosition().moveBy(5), widget.getCaretPosition());
-	}
-
-	@Test
-	public void moveToPreviousWord() throws Exception {
-		widget.insertElement(TITLE);
-		widget.moveBy(1);
-		final IElement paraElement = widget.insertElement(PARA);
-		widget.insertText("Test Word Another Word");
-		widget.moveTo(paraElement.getEndPosition().moveBy(1));
-		widget.moveToPreviousWord(false);
-		assertEquals(paraElement.getStartPosition().moveBy(19), widget.getCaretPosition());
-		widget.moveToPreviousWord(false);
-		assertEquals(paraElement.getStartPosition().moveBy(11), widget.getCaretPosition());
-	}
-}
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/L2ProcessingInstructionEditingTest.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/L2ProcessingInstructionEditingTest.java
index c5ab526..850c1b9 100644
--- a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/L2ProcessingInstructionEditingTest.java
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/L2ProcessingInstructionEditingTest.java
@@ -19,8 +19,8 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
-import org.eclipse.vex.core.internal.css.StyleSheet;
-import org.eclipse.vex.core.internal.undo.CannotRedoException;
+import org.eclipse.vex.core.internal.undo.CannotApplyException;
+import org.eclipse.vex.core.provisional.dom.IDocument;
 import org.eclipse.vex.core.provisional.dom.IDocumentFragment;
 import org.eclipse.vex.core.provisional.dom.INode;
 import org.eclipse.vex.core.provisional.dom.IProcessingInstruction;
@@ -29,93 +29,94 @@
 
 public class L2ProcessingInstructionEditingTest {
 
-	private IVexWidget widget;
+	private IDocumentEditor editor;
 
 	@Before
 	public void setUp() throws Exception {
-		widget = new BaseVexWidget(new MockHostComponent());
-		widget.setDocument(createDocumentWithDTD(TEST_DTD, "section"), StyleSheet.NULL);
+		final IDocument document = createDocumentWithDTD(TEST_DTD, "section");
+		editor = new DocumentEditor(new FakeCursor(document));
+		editor.setDocument(document);
 	}
 
 	@Test
 	public void insertProcessingInstruction() throws Exception {
-		widget.insertElement(TITLE);
-		widget.insertProcessingInstruction("target");
-		widget.insertText("data");
+		editor.insertElement(TITLE);
+		editor.insertProcessingInstruction("target");
+		editor.insertText("data");
 
-		widget.moveBy(-1);
-		final INode node = widget.getCurrentNode();
+		editor.moveBy(-1);
+		final INode node = editor.getCurrentNode();
 		assertTrue(node instanceof IProcessingInstruction);
 		final IProcessingInstruction pi = (IProcessingInstruction) node;
 		assertEquals("target", pi.getTarget());
 		assertEquals("data", pi.getText());
 	}
 
-	@Test(expected = CannotRedoException.class)
+	@Test(expected = CannotApplyException.class)
 	public void shouldNotInsertInvalidProcessingInstruction() throws Exception {
-		widget.insertElement(TITLE);
-		widget.insertProcessingInstruction(" tar get");
+		editor.insertElement(TITLE);
+		editor.insertProcessingInstruction(" tar get");
 	}
 
 	@Test
 	public void undoInsertProcessingInstructionWithSubsequentDelete() throws Exception {
-		widget.insertElement(PARA);
-		final String expectedXml = getCurrentXML(widget);
+		editor.insertElement(PARA);
+		final String expectedXml = getCurrentXML(editor);
 
-		final IProcessingInstruction pi = widget.insertProcessingInstruction("target");
+		final IProcessingInstruction pi = editor.insertProcessingInstruction("target");
 
-		widget.moveTo(pi.getStartPosition());
-		widget.moveTo(pi.getEndPosition(), true);
-		widget.deleteSelection();
+		editor.moveTo(pi.getStartPosition());
+		editor.moveTo(pi.getEndPosition(), true);
+		editor.deleteSelection();
 
-		widget.undo(); // delete
-		widget.undo(); // insert comment
+		editor.undo(); // delete
+		editor.undo(); // insert comment
 
-		assertEquals(expectedXml, getCurrentXML(widget));
+		assertEquals(expectedXml, getCurrentXML(editor));
 	}
 
 	@Test
 	public void undoRemoveProcessingInstruction() throws Exception {
-		widget.insertElement(TITLE);
-		widget.insertText("1text before pi");
-		final INode pi = widget.insertProcessingInstruction("target");
-		widget.insertText("2pi text2");
-		widget.moveBy(1);
-		widget.insertText("3text after pi");
+		editor.insertElement(TITLE);
+		editor.insertText("1text before pi");
+		final INode pi = editor.insertProcessingInstruction("target");
+		editor.insertText("2pi text2");
+		editor.moveBy(1);
+		editor.insertText("3text after pi");
 
-		final String expectedContentStructure = getContentStructure(widget.getDocument().getRootElement());
+		final String expectedContentStructure = getContentStructure(editor.getDocument().getRootElement());
 
-		widget.doWork(new Runnable() {
+		editor.doWork(new Runnable() {
 			@Override
 			public void run() {
-				widget.moveTo(pi.getStartPosition().moveBy(1), false);
-				widget.moveTo(pi.getEndPosition().moveBy(-1), true);
-				final IDocumentFragment fragment = widget.getSelectedFragment();
-				widget.deleteSelection();
+				editor.moveTo(pi.getStartPosition().moveBy(1), false);
+				editor.moveTo(pi.getEndPosition().moveBy(-1), true);
+				final IDocumentFragment fragment = editor.getSelectedFragment();
+				editor.deleteSelection();
 
-				widget.moveBy(-1, false);
-				widget.moveBy(1, true);
-				widget.deleteSelection();
+				editor.moveBy(-1, false);
+				editor.moveBy(1, true);
+				editor.deleteSelection();
 
-				widget.insertFragment(fragment);
+				editor.insertFragment(fragment);
 			}
 		});
 
-		widget.undo();
+		editor.undo();
 
-		assertEquals(expectedContentStructure, getContentStructure(widget.getDocument().getRootElement()));
+		assertEquals(expectedContentStructure, getContentStructure(editor.getDocument().getRootElement()));
 	}
 
 	@Test
 	public void editProcessingInstruction() throws Exception {
-		widget.insertElement(TITLE);
-		widget.insertProcessingInstruction("target");
-		widget.insertText("oldData");
+		editor.insertElement(TITLE);
+		editor.insertProcessingInstruction("target");
+		editor.insertText("oldData");
 
-		widget.moveBy(-1);
-		widget.editProcessingInstruction("new", "newData");
+		editor.moveBy(-1);
+		editor.editProcessingInstruction("new", "newData");
 
-		final INode node = widget.getCurrentNode();
+		final INode node = editor.getCurrentNode();
 		assertTrue(node instanceof IProcessingInstruction);
 		final IProcessingInstruction pi = (IProcessingInstruction) node;
 		assertEquals("new", pi.getTarget());
@@ -124,32 +125,32 @@
 
 	@Test
 	public void undoEditProcessingInstruction() throws Exception {
-		widget.insertElement(TITLE);
-		widget.insertText("1text before pi");
-		final IProcessingInstruction pi = widget.insertProcessingInstruction("target");
-		widget.insertText("2data");
-		widget.moveBy(1);
-		widget.insertText("3text after pi");
+		editor.insertElement(TITLE);
+		editor.insertText("1text before pi");
+		final IProcessingInstruction pi = editor.insertProcessingInstruction("target");
+		editor.insertText("2data");
+		editor.moveBy(1);
+		editor.insertText("3text after pi");
 
-		final String expectedContentStructure = getContentStructure(widget.getDocument().getRootElement());
+		final String expectedContentStructure = getContentStructure(editor.getDocument().getRootElement());
 
-		widget.moveTo(pi.getStartPosition().moveBy(1));
-		widget.editProcessingInstruction("new", "data");
-		widget.undo();
+		editor.moveTo(pi.getStartPosition().moveBy(1));
+		editor.editProcessingInstruction("new", "data");
+		editor.undo();
 
-		assertEquals(expectedContentStructure, getContentStructure(widget.getDocument().getRootElement()));
+		assertEquals(expectedContentStructure, getContentStructure(editor.getDocument().getRootElement()));
 	}
 
 	@Test
 	public void editProcessingInstruction_newTargetOnly() throws Exception {
-		widget.insertElement(TITLE);
-		widget.insertProcessingInstruction("target");
-		widget.insertText("oldData");
+		editor.insertElement(TITLE);
+		editor.insertProcessingInstruction("target");
+		editor.insertText("oldData");
 
-		widget.moveBy(-1);
-		widget.editProcessingInstruction("new", null);
+		editor.moveBy(-1);
+		editor.editProcessingInstruction("new", null);
 
-		final INode node = widget.getCurrentNode();
+		final INode node = editor.getCurrentNode();
 		assertTrue(node instanceof IProcessingInstruction);
 		final IProcessingInstruction pi = (IProcessingInstruction) node;
 		assertEquals("new", pi.getTarget());
@@ -158,14 +159,14 @@
 
 	@Test
 	public void editProcessingInstruction_newDataOnly() throws Exception {
-		widget.insertElement(TITLE);
-		widget.insertProcessingInstruction("target");
-		widget.insertText("oldData");
+		editor.insertElement(TITLE);
+		editor.insertProcessingInstruction("target");
+		editor.insertText("oldData");
 
-		widget.moveBy(-1);
-		widget.editProcessingInstruction(null, "newData");
+		editor.moveBy(-1);
+		editor.editProcessingInstruction(null, "newData");
 
-		final INode node = widget.getCurrentNode();
+		final INode node = editor.getCurrentNode();
 		assertTrue(node instanceof IProcessingInstruction);
 		final IProcessingInstruction pi = (IProcessingInstruction) node;
 		assertEquals("target", pi.getTarget());
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/L2SelectionTest.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/L2SelectionTest.java
index 2433996..cb7f1d2 100644
--- a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/L2SelectionTest.java
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/L2SelectionTest.java
@@ -1,247 +1,71 @@
-/*******************************************************************************

- * Copyright (c) 2012, 2014 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

- *      Carsten Hiesserich - additional tests

- *******************************************************************************/

-package org.eclipse.vex.core.internal.widget;

-

-import static org.eclipse.vex.core.internal.widget.VexWidgetTest.PARA;

-import static org.eclipse.vex.core.internal.widget.VexWidgetTest.PRE;

-import static org.eclipse.vex.core.internal.widget.VexWidgetTest.TITLE;

-import static org.eclipse.vex.core.internal.widget.VexWidgetTest.createDocumentWithDTD;

-import static org.eclipse.vex.core.tests.TestResources.TEST_DTD;

-import static org.junit.Assert.assertEquals;

-import static org.junit.Assert.assertFalse;

-import static org.junit.Assert.assertTrue;

-

-import org.eclipse.vex.core.internal.css.StyleSheet;

-import org.eclipse.vex.core.provisional.dom.IComment;

-import org.eclipse.vex.core.provisional.dom.IElement;

-import org.junit.Before;

-import org.junit.Test;

-

-/**

- * @author Florian Thienel

- */

-public class L2SelectionTest {

-

-	private IVexWidget widget;

-	private MockHostComponent hostComponent;

-

-	@Before

-	public void setUp() throws Exception {

-		hostComponent = new MockHostComponent();

-		widget = new BaseVexWidget(hostComponent);

-		widget.setDocument(createDocumentWithDTD(TEST_DTD, "section"), StyleSheet.NULL);

-	}

-

-	@Test

-	public void givenCaretInElement_whenSelectionIncludesStartOffset_shouldExpandSelectionToEndOffset() throws Exception {

-		final IElement titleElement = widget.insertElement(TITLE);

-		widget.moveBy(-1, true);

-		assertTrue(widget.hasSelection());

-		assertEquals(titleElement.getRange(), widget.getSelectedRange());

-		assertEquals(titleElement.getStartPosition(), widget.getCaretPosition());

-	}

-

-	@Test

-	public void givenCaretInElementWith_whenSelectionIncludesStartOffset_shouldExpandSelectionToEndOffset() throws Exception {

-		final IElement titleElement = widget.insertElement(TITLE);

-		widget.insertText("Hello World");

-		widget.moveBy(-5, false);

-		widget.moveTo(titleElement.getStartPosition(), true);

-		assertTrue(widget.hasSelection());

-		assertEquals(titleElement.getRange(), widget.getSelectedRange());

-		assertEquals(titleElement.getStartPosition(), widget.getCaretPosition());

-	}

-

-	@Test

-	public void givenCaretInElementWith_whenSelectionForwardIncludesStartOffset_shouldExpandSelectionToEndOffset() throws Exception {

-		widget.insertElement(PARA);

-		widget.insertText("before");

-		final IElement innerElement = widget.insertElement(PRE);

-		widget.insertText("Selection");

-		widget.moveTo(innerElement.getEndPosition().moveBy(1));

-		widget.insertText("after");

-

-		widget.moveTo(innerElement.getStartPosition().moveBy(-1));

-		widget.moveTo(innerElement.getStartPosition().moveBy(1), true);

-

-		assertTrue(widget.hasSelection());

-		assertEquals(innerElement.getStartPosition().moveBy(-1), widget.getSelectedPositionRange().getStartPosition());

-		assertEquals(innerElement.getEndPosition().moveBy(1), widget.getSelectedPositionRange().getEndPosition());

-		assertEquals(innerElement.getEndPosition().moveBy(1), widget.getCaretPosition());

-	}

-

-	@Test

-	public void givenCaretInElementWith_whenSelectionBackwardIncludesStartOffset_shouldExpandSelectionToEndOffset() throws Exception {

-		widget.insertElement(PARA);

-		final IElement innerElement = widget.insertElement(PRE);

-		widget.insertText("Selection");

-		widget.moveTo(innerElement.getEndPosition().moveBy(1));

-		widget.insertText("after");

-		widget.moveTo(innerElement.getEndPosition().moveBy(-1));

-		widget.moveTo(innerElement.getStartPosition(), true);

-

-		assertTrue(widget.hasSelection());

-		assertEquals(innerElement.getRange(), widget.getSelectedRange());

-		assertEquals(innerElement.getStartPosition(), widget.getCaretPosition());

-	}

-

-	@Test

-	public void givenCaretInElementAtEndOffset_whenMovedByOneBehindEndOffset_shouldExpandSelectionToStartOffset() throws Exception {

-		final IElement titleElement = widget.insertElement(TITLE);

-		widget.insertText("Hello World");

-		widget.moveBy(1, true);

-		assertTrue(widget.hasSelection());

-		assertEquals(titleElement.getRange(), widget.getSelectedRange());

-		assertEquals(titleElement.getEndPosition().moveBy(1), widget.getCaretPosition());

-	}

-

-	@Test

-	public void givenCaretInElementAtEndOffset_whenMovedOneBehindStartOffset_shouldNotIncludeEndOffsetInSelectedRange() throws Exception {

-		final IElement titleElement = widget.insertElement(TITLE);

-		widget.insertText("Hello World");

-		widget.moveTo(titleElement.getStartPosition().moveBy(1), true);

-		assertEquals(titleElement.getRange().resizeBy(1, -1), widget.getSelectedRange());

-		assertEquals(titleElement.getStartPosition().moveBy(1), widget.getCaretPosition());

-	}

-

-	@Test

-	public void givenCaretAtStartOffsetOfElementWithText_whenMovedByOneForward_shouldExpandSelectionBehindEndOffset() throws Exception {

-		final IElement titleElement = widget.insertElement(TITLE);

-		widget.insertText("Hello World");

-		widget.moveTo(titleElement.getStartPosition(), false);

-		widget.moveBy(1, true);

-		assertEquals(titleElement.getRange(), widget.getSelectedRange());

-		assertEquals(titleElement.getEndPosition().moveBy(1), widget.getCaretPosition());

-	}

-

-	@Test

-	public void givenCaretAtStartOffsetOfElementWithText_whenMovedOneForwardAndOneBackward_shouldSelectNothing() throws Exception {

-		final IElement titleElement = widget.insertElement(TITLE);

-		widget.insertText("Hello World");

-		widget.moveTo(titleElement.getStartPosition(), false);

-		widget.moveBy(1, true);

-		widget.moveBy(-1, true);

-		assertEquals(titleElement.getStartPosition(), widget.getCaretPosition());

-	}

-

-	@Test

-	public void givenCaretInElementWithText_whenMovedBehindFollowingElementAndMovedBackOnce_shouldSelectOnlyFirstElement() throws Exception {

-		final IElement titleElement = widget.insertElement(TITLE);

-		widget.insertText("Hello World");

-		widget.moveBy(1);

-		final IElement paraElement = widget.insertElement(PARA);

-		widget.insertText("Hello Again");

-		widget.moveTo(titleElement.getStartPosition().moveBy(3));

-		widget.moveTo(paraElement.getEndPosition().moveBy(1), true);

-		widget.moveBy(-1, true);

-		assertEquals(titleElement.getRange(), widget.getSelectedRange());

-		assertEquals(titleElement.getEndPosition().moveBy(1), widget.getCaretPosition());

-	}

-

-	@Test

-	public void givenCaretInElementWithText_whenMovedBehindFollowingElementAndMovedBackTwice_shouldSelectOnlyTextFragementOfFirstElement() throws Exception {

-		final IElement titleElement = widget.insertElement(TITLE);

-		widget.insertText("Hello World");

-		widget.moveBy(1);

-		final IElement paraElement = widget.insertElement(PARA);

-		widget.insertText("Hello Again");

-		widget.moveTo(titleElement.getStartPosition().moveBy(3));

-		widget.moveTo(paraElement.getEndPosition().moveBy(1), true);

-		widget.moveBy(-1, true);

-		widget.moveBy(-1, true);

-		assertEquals(titleElement.getRange().resizeBy(3, -1), widget.getSelectedRange());

-		assertEquals(titleElement.getEndPosition(), widget.getCaretPosition());

-	}

-

-	@Test

-	public void givenCaretInElementWithText_whenMovedBeforePrecedingElementAndMovedForwardOnce_shouldSelectOnlySecondElement() throws Exception {

-		final IElement titleElement = widget.insertElement(TITLE);

-		widget.insertText("Hello World");

-		widget.moveBy(1);

-		final IElement paraElement = widget.insertElement(PARA);

-		widget.insertText("Hello Again");

-		widget.moveTo(paraElement.getEndPosition().moveBy(-3));

-		widget.moveTo(titleElement.getStartPosition(), true);

-		widget.moveBy(1, true);

-		assertEquals(paraElement.getRange(), widget.getSelectedRange());

-		assertEquals(paraElement.getStartPosition(), widget.getCaretPosition());

-	}

-

-	@Test

-	public void givenCaretInElementWithText_whenMovedBeforePrecedingElementAndMovedForwardTwice_shouldSelectOnlyTextFragementOfSecondElement() throws Exception {

-		final IElement titleElement = widget.insertElement(TITLE);

-		widget.insertText("Hello World");

-		widget.moveBy(1);

-		final IElement paraElement = widget.insertElement(PARA);

-		widget.insertText("Hello Again");

-		widget.moveTo(paraElement.getEndPosition().moveBy(-3));

-		widget.moveTo(titleElement.getStartPosition(), true);

-		widget.moveBy(1, true);

-		widget.moveBy(1, true);

-		assertEquals(paraElement.getRange().resizeBy(1, -4), widget.getSelectedRange());

-		assertEquals(paraElement.getStartPosition().moveBy(+1), widget.getCaretPosition());

-	}

-

-	@Test

-	public void givenCarentInEmptyComment_whenMovedBeforeComment_shouldExpandSelectionToIncludeEndOffset() throws Exception {

-		final IComment comment = widget.insertComment();

-		widget.moveBy(-1, true);

-		assertTrue(widget.hasSelection());

-		assertEquals(comment.getRange(), widget.getSelectedRange());

-		assertEquals(comment.getStartPosition(), widget.getCaretPosition());

-	}

-

-	@Test

-	public void givenNodeInDocument_whenNodeHasContent_shouldSelectContentOfNode() throws Exception {

-		final IElement title = widget.insertElement(TITLE);

-		widget.insertText("Hello World");

-

-		widget.selectContentOf(title);

-

-		assertEquals(title.getRange().resizeBy(1, -1), widget.getSelectedRange());

-		assertTrue(widget.hasSelection());

-	}

-

-	@Test

-	public void givenNodeInDocument_whenNodeIsEmpty_shouldMoveCaretIntoNode() throws Exception {

-		final IElement title = widget.insertElement(TITLE);

-

-		widget.selectContentOf(title);

-

-		assertEquals(title.getEndPosition(), widget.getCaretPosition());

-		assertFalse(widget.hasSelection());

-	}

-

-	@Test

-	public void givenNodeInDocument_shouldSelectCompleteNodeWithStartAndEndTags() throws Exception {

-		final IElement title = widget.insertElement(TITLE);

-		widget.insertText("Hello World");

-

-		widget.select(title);

-

-		assertEquals(title.getRange(), widget.getSelectedRange());

-		assertTrue(widget.hasSelection());

-	}

-

-	@Test

-	public void givenWorkBlockStarted_whenWorkBlockNotEnded_shouldNotFireSelectionChangedEvent() throws Exception {

-		hostComponent.selectionChanged = false;

-		widget.beginWork();

-		final IElement title = widget.insertElement(TITLE);

-		widget.moveTo(title.getStartPosition());

-		widget.moveTo(title.getEndPosition(), true);

-		final boolean selectionChangedWhileWorking = hostComponent.selectionChanged;

-		widget.endWork(true);

-

-		assertFalse(selectionChangedWhileWorking);

-	}

-}

+/*******************************************************************************
+ * Copyright (c) 2012, 2014 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
+ *      Carsten Hiesserich - additional tests
+ *******************************************************************************/
+package org.eclipse.vex.core.internal.widget;
+
+import static org.eclipse.vex.core.internal.widget.VexWidgetTest.TITLE;
+import static org.eclipse.vex.core.internal.widget.VexWidgetTest.createDocumentWithDTD;
+import static org.eclipse.vex.core.tests.TestResources.TEST_DTD;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.eclipse.vex.core.provisional.dom.IDocument;
+import org.eclipse.vex.core.provisional.dom.IElement;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author Florian Thienel
+ */
+public class L2SelectionTest {
+
+	private IDocumentEditor editor;
+
+	@Before
+	public void setUp() throws Exception {
+		final IDocument document = createDocumentWithDTD(TEST_DTD, "section");
+		editor = new DocumentEditor(new FakeCursor(document));
+		editor.setDocument(document);
+	}
+
+	@Test
+	public void givenNodeInDocument_whenNodeHasContent_shouldSelectContentOfNode() throws Exception {
+		final IElement title = editor.insertElement(TITLE);
+		editor.insertText("Hello World");
+
+		editor.selectContentOf(title);
+
+		assertEquals(title.getRange().resizeBy(1, 0), editor.getSelectedRange());
+		assertTrue(editor.hasSelection());
+	}
+
+	@Test
+	public void givenNodeInDocument_whenNodeIsEmpty_shouldMoveCaretIntoNode() throws Exception {
+		final IElement title = editor.insertElement(TITLE);
+
+		editor.selectContentOf(title);
+
+		assertEquals(title.getEndPosition(), editor.getCaretPosition());
+		assertFalse(editor.hasSelection());
+	}
+
+	@Test
+	public void givenNodeInDocument_shouldSelectCompleteNodeWithStartAndEndTags() throws Exception {
+		final IElement title = editor.insertElement(TITLE);
+		editor.insertText("Hello World");
+
+		editor.select(title);
+
+		assertEquals(title.getRange().resizeBy(0, 1), editor.getSelectedRange());
+		assertTrue(editor.hasSelection());
+	}
+}
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/L2SimpleEditingTest.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/L2SimpleEditingTest.java
index c8e69c2..008c29c 100644
--- a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/L2SimpleEditingTest.java
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/L2SimpleEditingTest.java
@@ -1,1140 +1,1147 @@
-/*******************************************************************************

- * Copyright (c) 2012, 2013 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

- * 		Carsten Hiesserich - additional tests (bug 407827, 409032)

- *******************************************************************************/

-package org.eclipse.vex.core.internal.widget;

-

-import static org.eclipse.vex.core.internal.widget.VexWidgetTest.PARA;

-import static org.eclipse.vex.core.internal.widget.VexWidgetTest.PRE;

-import static org.eclipse.vex.core.internal.widget.VexWidgetTest.SECTION;

-import static org.eclipse.vex.core.internal.widget.VexWidgetTest.TITLE;

-import static org.eclipse.vex.core.internal.widget.VexWidgetTest.assertCanMorphOnlyTo;

-import static org.eclipse.vex.core.internal.widget.VexWidgetTest.assertXmlEquals;

-import static org.eclipse.vex.core.internal.widget.VexWidgetTest.createDocumentWithDTD;

-import static org.eclipse.vex.core.internal.widget.VexWidgetTest.getCurrentXML;

-import static org.eclipse.vex.core.tests.TestResources.TEST_DTD;

-import static org.junit.Assert.assertEquals;

-import static org.junit.Assert.assertFalse;

-import static org.junit.Assert.assertNull;

-import static org.junit.Assert.assertSame;

-import static org.junit.Assert.assertTrue;

-

-import java.io.IOException;

-import java.util.List;

-

-import org.eclipse.vex.core.internal.css.CssWhitespacePolicy;

-import org.eclipse.vex.core.internal.css.StyleSheet;

-import org.eclipse.vex.core.internal.css.StyleSheetReader;

-import org.eclipse.vex.core.internal.undo.CannotRedoException;

-import org.eclipse.vex.core.provisional.dom.DocumentValidationException;

-import org.eclipse.vex.core.provisional.dom.IComment;

-import org.eclipse.vex.core.provisional.dom.IDocumentFragment;

-import org.eclipse.vex.core.provisional.dom.IElement;

-import org.eclipse.vex.core.provisional.dom.INode;

-import org.eclipse.vex.core.provisional.dom.IParent;

-import org.eclipse.vex.core.provisional.dom.IText;

-import org.eclipse.vex.core.tests.TestResources;

-import org.junit.Before;

-import org.junit.Test;

-

-/**

- * @author Florian Thienel

- */

-public class L2SimpleEditingTest {

-

-	private IVexWidget widget;

-	private IElement rootElement;

-

-	@Before

-	public void setUp() throws Exception {

-		widget = new BaseVexWidget(new MockHostComponent());

-		widget.setDocument(createDocumentWithDTD(TEST_DTD, "section"), readTestStyleSheet());

-		widget.setWhitespacePolicy(new CssWhitespacePolicy(widget.getStyleSheet()));

-		rootElement = widget.getDocument().getRootElement();

-	}

-

-	@Test

-	public void shouldStartInRootElement() throws Exception {

-		assertSame(rootElement, widget.getCurrentElement());

-		assertEquals(rootElement.getEndPosition(), widget.getCaretPosition());

-	}

-

-	@Test

-	public void shouldMoveCaretIntoInsertedElement() throws Exception {

-		final IElement titleElement = widget.insertElement(TITLE);

-		assertEquals(titleElement.getEndPosition(), widget.getCaretPosition());

-	}

-

-	@Test

-	public void shouldProvideInsertionElementAsCurrentElement() throws Exception {

-		final IElement titleElement = widget.insertElement(TITLE);

-		widget.moveBy(-1);

-		assertEquals(titleElement.getStartPosition(), widget.getCaretPosition());

-		assertSame(rootElement, widget.getCurrentElement());

-	}

-

-	@Test

-	public void givenAnElementWithText_whenAtEndOfTextAndHittingBackspace_shouldDeleteLastCharacter() throws Exception {

-		final IElement titleElement = widget.insertElement(TITLE);

-		widget.insertText("Hello");

-		widget.deletePreviousChar();

-		assertEquals("Hell", titleElement.getText());

-		assertEquals(titleElement.getEndPosition(), widget.getCaretPosition());

-	}

-

-	@Test

-	public void givenAnElementWithText_whenAtBeginningOfTextAndHittingDelete_shouldDeleteFirstCharacter() throws Exception {

-		final IElement titleElement = widget.insertElement(TITLE);

-		widget.insertText("Hello");

-		widget.moveBy(-5);

-		widget.deleteNextChar();

-		assertEquals("ello", titleElement.getText());

-		assertEquals(titleElement.getStartPosition().moveBy(1), widget.getCaretPosition());

-	}

-

-	@Test

-	public void givenAnEmptyElement_whenCaretBetweenStartAndEndTagAndHittingBackspace_shouldDeleteEmptyElement() throws Exception {

-		widget.insertElement(TITLE);

-		widget.moveBy(1);

-		final IElement paraElement = widget.insertElement(PARA);

-		widget.deletePreviousChar();

-		assertEquals(1, rootElement.children().count());

-		assertNull(paraElement.getParent());

-		assertFalse(paraElement.isAssociated());

-	}

-

-	@Test

-	public void givenAnEmptyElement_whenCaretBetweenStartAndEndTagAndHittingDelete_shouldDeleteEmptyElement() throws Exception {

-		widget.insertElement(TITLE);

-		widget.moveBy(1);

-		final IElement paraElement = widget.insertElement(PARA);

-		widget.deleteNextChar();

-		assertEquals(1, rootElement.children().count());

-		assertNull(paraElement.getParent());

-		assertFalse(paraElement.isAssociated());

-	}

-

-	@Test

-	public void givenAnEmptyElement_whenCaretAfterEndTagAndHittingDelete_shouldDeleteEmptyElement() throws Exception {

-		widget.insertElement(TITLE);

-		widget.moveBy(1);

-		final IElement paraElement = widget.insertElement(PARA);

-		widget.moveBy(1);

-		widget.deletePreviousChar();

-		assertEquals(1, rootElement.children().count());

-		assertNull(paraElement.getParent());

-		assertFalse(paraElement.isAssociated());

-	}

-

-	@Test

-	public void givenAnEmptyElement_whenCaretBeforeStartTagAndHittingDelete_shouldDeleteEmptyElement() throws Exception {

-		widget.insertElement(TITLE);

-		widget.moveBy(1);

-		final IElement paraElement = widget.insertElement(PARA);

-		widget.moveBy(-1);

-		widget.deleteNextChar();

-		assertEquals(1, rootElement.children().count());

-		assertNull(paraElement.getParent());

-		assertFalse(paraElement.isAssociated());

-	}

-

-	@Test

-	public void givenTwoMatchingElements_whenCaretBetweenEndAndStartTagAndHittingBackspace_shouldJoinElements() throws Exception {

-		widget.insertElement(TITLE);

-		widget.moveBy(1);

-		final IElement para1 = widget.insertElement(PARA);

-		widget.insertText("Hello");

-		widget.moveBy(1);

-		final IElement para2 = widget.insertElement(PARA);

-		widget.insertText("World");

-

-		widget.moveTo(para2.getStartPosition());

-		widget.deletePreviousChar();

-

-		assertEquals(2, rootElement.children().count());

-		assertSame(rootElement, para1.getParent());

-		assertTrue(para1.isAssociated());

-		assertEquals("HelloWorld", para1.getText());

-		assertNull(para2.getParent());

-		assertFalse(para2.isAssociated());

-	}

-

-	@Test

-	public void givenTwoMatchingElements_whenCaretBetweenEndAndStartTagAndHittingDelete_shouldJoinElements() throws Exception {

-		widget.insertElement(TITLE);

-		widget.moveBy(1);

-		final IElement para1 = widget.insertElement(PARA);

-		widget.insertText("Hello");

-		widget.moveBy(1);

-		final IElement para2 = widget.insertElement(PARA);

-		widget.insertText("World");

-

-		widget.moveTo(para2.getStartPosition());

-		widget.deleteNextChar();

-

-		assertEquals(2, rootElement.children().count());

-		assertSame(rootElement, para1.getParent());

-		assertTrue(para1.isAssociated());

-		assertEquals("HelloWorld", para1.getText());

-		assertNull(para2.getParent());

-		assertFalse(para2.isAssociated());

-	}

-

-	@Test

-	public void givenTwoMatchingElements_whenCaretAfterStartTagOfSecondElementAndHittingBackspace_shouldJoinElements() throws Exception {

-		widget.insertElement(TITLE);

-		widget.moveBy(1);

-		final IElement para1 = widget.insertElement(PARA);

-		widget.insertText("Hello");

-		widget.moveBy(1);

-		final IElement para2 = widget.insertElement(PARA);

-		widget.insertText("World");

-

-		widget.moveTo(para2.getStartPosition().moveBy(1));

-		widget.deletePreviousChar();

-

-		assertEquals(2, rootElement.children().count());

-		assertSame(rootElement, para1.getParent());

-		assertTrue(para1.isAssociated());

-		assertEquals("HelloWorld", para1.getText());

-		assertNull(para2.getParent());

-		assertFalse(para2.isAssociated());

-	}

-

-	@Test

-	public void givenTwoMatchingElements_whenCaretBeforeEndTagOfFirstElementAndHittingDelete_shouldJoinElements() throws Exception {

-		widget.insertElement(TITLE);

-		widget.moveBy(1);

-		final IElement para1 = widget.insertElement(PARA);

-		widget.insertText("Hello");

-		widget.moveBy(1);

-		final IElement para2 = widget.insertElement(PARA);

-		widget.insertText("World");

-

-		widget.moveTo(para1.getEndPosition());

-		widget.deleteNextChar();

-

-		assertEquals(2, rootElement.children().count());

-		assertSame(rootElement, para1.getParent());

-		assertTrue(para1.isAssociated());

-		assertEquals("HelloWorld", para1.getText());

-		assertNull(para2.getParent());

-		assertFalse(para2.isAssociated());

-	}

-

-	@Test

-	public void givenElementWithText_whenAllTextSelectedAndInsertingACharacter_shouldReplaceAllTextWithNewCharacter() throws Exception {

-		final IElement titleElement = widget.insertElement(TITLE);

-		widget.insertText("Hello World");

-

-		widget.selectContentOf(titleElement);

-		widget.insertChar('A');

-

-		assertEquals("A", titleElement.getText());

-	}

-

-	@Test(expected = ReadOnlyException.class)

-	public void whenReadOnly_shouldNotInsertText() throws Exception {

-		widget.insertElement(TITLE); // need an element where text would be valid

-		widget.setReadOnly(true);

-		assertFalse(widget.canInsertText());

-		widget.insertText("Hello World");

-	}

-

-	@Test(expected = ReadOnlyException.class)

-	public void whenReadOnly_shouldNotInsertChar() throws Exception {

-		widget.insertElement(TITLE); // need an element where text would be valid

-		widget.setReadOnly(true);

-		assertFalse(widget.canInsertText());

-		widget.insertChar('H');

-	}

-

-	@Test(expected = ReadOnlyException.class)

-	public void whenReadOnly_shouldNotInsertElement() throws Exception {

-		widget.setReadOnly(true);

-		assertTrue(widget.getValidInsertElements().length == 0);

-		widget.insertElement(TITLE);

-	}

-

-	@Test(expected = ReadOnlyException.class)

-	public void whenReadOnly_shouldNotInsertComment() throws Exception {

-		widget.setReadOnly(true);

-		assertFalse(widget.canInsertComment());

-		widget.insertComment();

-	}

-

-	@Test(expected = ReadOnlyException.class)

-	public void whenReadOnly_shouldNotInsertFragment() throws Exception {

-		widget.insertElement(TITLE);

-		widget.moveBy(1);

-		final IElement para = widget.insertElement(PARA);

-		final IDocumentFragment fragment = widget.getDocument().getFragment(para.getRange());

-		widget.moveTo(rootElement.getEndPosition());

-

-		widget.setReadOnly(true);

-		assertFalse(widget.canInsertFragment(fragment));

-		widget.insertFragment(fragment);

-	}

-

-	@Test

-	public void givenNonPreElement_whenInsertingNewline_shouldSplitElement() throws Exception {

-		widget.insertElement(TITLE);

-		widget.moveBy(1);

-		final IElement para1 = widget.insertElement(PARA);

-		widget.insertText("Hello");

-		widget.moveTo(para1.getEndPosition());

-		widget.insertText("\n");

-		assertEquals("Hello", para1.getText());

-		assertEquals(3, rootElement.children().count());

-	}

-

-	@Test

-	public void givenPreElement_whenInsertingNewline_shouldInsertNewline() throws Exception {

-		final IElement para = widget.insertElement(PARA);

-		final IElement preElement = widget.insertElement(PRE);

-		assertEquals(preElement.getEndPosition(), widget.getCaretPosition());

-		widget.insertText("line1");

-		widget.insertText("\n");

-		assertEquals(1, para.children().count());

-		assertEquals("line1\n", preElement.getText());

-	}

-

-	@Test

-	public void insertingTextAtInvalidPosition_shouldNotAlterDocument() throws Exception {

-		final IElement para1 = widget.insertElement(PARA);

-		widget.moveBy(1);

-		final IElement para2 = widget.insertElement(PARA);

-		widget.moveTo(para1.getEndPosition());

-		widget.insertText("Para1");

-		widget.moveTo(para2.getEndPosition());

-		widget.insertText("Para2");

-

-		// Insert position is invalid

-		widget.moveTo(para1.getEndPosition().moveBy(1));

-		try {

-			widget.insertText("Test");

-		} catch (final Exception ex) {

-		} finally {

-			assertEquals("Para1", para1.getText());

-			assertEquals("Para2", para2.getText());

-		}

-	}

-

-	@Test

-	public void insertingElementAtInvalidPosition_shouldNotAlterDocument() throws Exception {

-		final IElement para1 = widget.insertElement(PARA);

-		widget.moveBy(1);

-		final IElement para2 = widget.insertElement(PARA);

-		widget.moveTo(para1.getEndPosition());

-		widget.insertText("Para1");

-		widget.moveTo(para2.getEndPosition());

-		widget.insertText("Para2");

-

-		// Insert position is invalid

-		widget.moveTo(para1.getEndPosition());

-		try {

-			widget.insertElement(PARA);

-		} catch (final Exception ex) {

-		} finally {

-			assertEquals("Para1", para1.getText());

-			assertEquals("Para2", para2.getText());

-		}

-	}

-

-	@Test

-	public void givenPreElement_whenInsertingTextWithNewline_shouldInsertNewline() throws Exception {

-		widget.insertElement(PARA);

-		final IElement preElement = widget.insertElement(PRE);

-		assertEquals(preElement.getEndPosition(), widget.getCaretPosition());

-		widget.insertText("line1\nline2");

-		assertEquals("line1\nline2", preElement.getText());

-	}

-

-	@Test

-	public void givenPreElement_whenInsertingText_shouldKeepWhitespace() throws Exception {

-		widget.insertElement(PARA);

-		final IElement preElement = widget.insertElement(PRE);

-

-		widget.moveTo(preElement.getEndPosition());

-		widget.insertText("line1\nline2   end");

-

-		final List<? extends INode> children = preElement.children().asList();

-		assertTrue("Expecting IText", children.get(0) instanceof IText);

-		assertEquals("line1\nline2   end", children.get(0).getText());

-	}

-

-	@Test

-	public void givenNonPreElement_whenInsertingText_shouldCompressWhitespace() throws Exception {

-		final IElement para = widget.insertElement(PARA);

-		widget.moveTo(para.getEndPosition());

-		widget.insertText("line1\nline2   \t end");

-

-		final List<? extends INode> children = rootElement.children().after(para.getStartPosition().getOffset()).asList();

-		assertEquals("two para elements", 2, children.size());

-		assertTrue("original para element", children.get(0) instanceof IParent);

-		assertTrue("splitted para element", children.get(1) instanceof IParent);

-		assertEquals("first line", "line1", children.get(0).getText());

-		assertEquals("second line with compressed whitespace", "line2 end", children.get(1).getText());

-	}

-

-	@Test

-	public void givenNonPreElement_whenInsertingText_shouldCompressNewlines() throws Exception {

-		final IElement para = widget.insertElement(PARA);

-		widget.moveTo(para.getEndPosition());

-		widget.insertText("line1\n\nline2");

-

-		final List<? extends INode> children = rootElement.children().after(para.getStartPosition().getOffset()).asList();

-		assertEquals("two para elements", 2, children.size());

-		assertTrue("original para element", children.get(0) instanceof IParent);

-		assertTrue("splitted para element", children.get(1) instanceof IParent);

-		assertEquals("first line", "line1", children.get(0).getText());

-		assertEquals("second line", "line2", children.get(1).getText());

-	}

-

-	@Test

-	public void givenNonPreElement_whenSplitting_shouldSplitIntoTwoElements() throws Exception {

-		final IElement para = widget.insertElement(PARA);

-		widget.moveTo(para.getEndPosition());

-		widget.insertText("line1line2");

-		widget.moveBy(-5);

-

-		widget.split();

-

-		final List<? extends INode> children = rootElement.children().after(para.getStartPosition().getOffset()).asList();

-		assertEquals("two para elements", 2, children.size());

-		assertTrue("original para element", children.get(0) instanceof IParent);

-		assertTrue("splitted para element", children.get(1) instanceof IParent);

-		assertEquals("first line element", PARA, ((IElement) children.get(0)).getQualifiedName());

-		assertEquals("second line element", PARA, ((IElement) children.get(0)).getQualifiedName());

-		assertEquals("first line", "line1", children.get(0).getText());

-		assertEquals("second line", "line2", children.get(1).getText());

-	}

-

-	@Test

-	public void givenPreElement_whenSplitting_shouldSplitIntoTwoElements() throws Exception {

-		final IElement para = widget.insertElement(PARA);

-		final IElement preElement = widget.insertElement(PRE);

-		widget.moveTo(preElement.getEndPosition());

-		widget.insertText("line1line2");

-		widget.moveBy(-5);

-

-		widget.split();

-

-		final List<? extends INode> children = para.children().after(preElement.getStartPosition().getOffset()).asList();

-		assertEquals("two para elements", 2, children.size());

-		assertTrue("original pre element", children.get(0) instanceof IParent);

-		assertTrue("splitted pre element", children.get(1) instanceof IParent);

-		assertEquals("first line element", PRE, ((IElement) children.get(0)).getQualifiedName());

-		assertEquals("second line element", PRE, ((IElement) children.get(0)).getQualifiedName());

-		assertEquals("first line", "line1", children.get(0).getText());

-		assertEquals("second line", "line2", children.get(1).getText());

-	}

-

-	@Test

-	public void givenElementWithMultipleOccurrence_canSplitElement() throws Exception {

-		widget.insertElement(PARA);

-		assertTrue(widget.canSplit());

-	}

-

-	@Test

-	public void givenElementWithMultipleOccurrence_whenAnythingIsSelected_canSplitElement() throws Exception {

-		widget.insertElement(PARA);

-		widget.insertText("12345");

-		widget.moveBy(-3, true);

-		assertTrue(widget.canSplit());

-	}

-

-	@Test

-	public void givenElementWithMultipleOccurrence_whenSplittingWithAnythingSelected_shouldDeleteSelectionAndSplit() throws Exception {

-		widget.insertElement(PARA);

-		widget.insertText("12345");

-		widget.moveBy(-3, true);

-		widget.split();

-

-		final List<? extends INode> children = rootElement.children().asList();

-		assertEquals("two para elements", 2, children.size());

-		assertTrue("original para element", children.get(0) instanceof IParent);

-		assertTrue("splitted para element", children.get(1) instanceof IParent);

-		assertEquals("first line element", PARA, ((IElement) children.get(0)).getQualifiedName());

-		assertEquals("second line element", PARA, ((IElement) children.get(0)).getQualifiedName());

-		assertEquals("first line", "12", children.get(0).getText());

-		assertEquals("second line", "", children.get(1).getText());

-	}

-

-	@Test

-	public void givenElementWithMultipleOccurrence_whenCaretRightAfterStartIndex_shouldSplit() throws Exception {

-		final IElement para = widget.insertElement(PARA);

-		widget.insertText("12345");

-		widget.moveTo(para.getStartPosition().moveBy(1));

-		widget.split();

-

-		final List<? extends INode> children = rootElement.children().asList();

-		assertEquals("two para elements", 2, children.size());

-		assertTrue("original para element", children.get(0) instanceof IParent);

-		assertTrue("splitted para element", children.get(1) instanceof IParent);

-		assertEquals("first line element", PARA, ((IElement) children.get(0)).getQualifiedName());

-		assertEquals("second line element", PARA, ((IElement) children.get(0)).getQualifiedName());

-		assertEquals("first line", "", children.get(0).getText());

-		assertEquals("second line", "12345", children.get(1).getText());

-	}

-

-	@Test

-	public void givenElementWithMultipleOccurrenceAndInlineElement_whenCaretAtInlineStartOffset_shouldSplit() throws Exception {

-		widget.insertElement(PARA);

-		widget.insertText("12");

-		widget.insertElement(PRE);

-		widget.insertText("34");

-		widget.moveBy(1);

-		widget.insertText("56");

-		widget.moveBy(-6);

-		widget.split();

-

-		final List<? extends INode> children = rootElement.children().asList();

-		assertEquals("two para elements", 2, children.size());

-		assertTrue("original para element", children.get(0) instanceof IParent);

-		assertTrue("splitted para element", children.get(1) instanceof IParent);

-		assertEquals("first line element", PARA, ((IElement) children.get(0)).getQualifiedName());

-		assertEquals("second line element", PARA, ((IElement) children.get(0)).getQualifiedName());

-		assertEquals("first line", "12", children.get(0).getText());

-		assertEquals("second line", "3456", children.get(1).getText());

-		assertEquals("pre in second line", PRE, ((IElement) ((IElement) children.get(1)).children().get(0)).getQualifiedName());

-	}

-

-	@Test

-	public void undoSubsequentSplitsOfInlineAndBlock() throws Exception {

-		widget.insertElement(PARA);

-		widget.insertText("12");

-		widget.insertElement(PRE);

-		widget.insertText("34");

-		widget.moveBy(1);

-		widget.insertText("56");

-		final String expectedXml = getCurrentXML(widget);

-

-		widget.moveBy(-4);

-		widget.split();

-

-		widget.moveBy(-1);

-		widget.split();

-

-		widget.undo();

-		widget.undo();

-

-		assertXmlEquals(expectedXml, widget);

-	}

-

-	@Test

-	public void givenElementWithSingleOccurrence_cannotSplitElement() throws Exception {

-		widget.insertElement(TITLE);

-		assertFalse(widget.canSplit());

-	}

-

-	@Test(expected = CannotRedoException.class)

-	public void givenElementWithSingleOccurrence_whenSplitting_shouldThrowCannotRedoException() throws Exception {

-		widget.insertElement(TITLE);

-		widget.split();

-	}

-

-	@Test

-	public void givenComment_cannotSplit() throws Exception {

-		widget.insertComment();

-		assertFalse(widget.canSplit());

-	}

-

-	@Test(expected = DocumentValidationException.class)

-	public void givenComment_whenSplitting_shouldThrowDocumentValidationException() throws Exception {

-		widget.insertComment();

-		widget.split();

-	}

-

-	@Test

-	public void givenBeforeRootElement_cannotSplitElement() throws Exception {

-		widget.moveTo(rootElement.getStartPosition());

-		assertFalse(widget.canSplit());

-	}

-

-	@Test(expected = DocumentValidationException.class)

-	public void givenBeforeRootElement_whenSplitting_shouldThrowDocumentValidationException() throws Exception {

-		widget.moveTo(rootElement.getStartPosition());

-		widget.split();

-	}

-

-	@Test

-	public void undoRedoChangeNamespaceWithSubsequentDelete() throws Exception {

-		widget.insertElement(TITLE);

-		widget.moveBy(1);

-		final IElement para = widget.insertElement(PARA);

-		final String expectedXml = getCurrentXML(widget);

-

-		widget.declareNamespace("ns1", "nsuri1");

-		widget.select(para);

-		widget.deleteSelection();

-

-		widget.undo(); // delete

-		widget.undo(); // declare namespace

-

-		assertXmlEquals(expectedXml, widget);

-	}

-

-	@Test

-	public void undoRedoChangeAttributeWithSubsequentDelete() throws Exception {

-		widget.insertElement(TITLE);

-		widget.moveBy(1);

-		final IElement para = widget.insertElement(PARA);

-		final String expectedXml = getCurrentXML(widget);

-

-		widget.setAttribute("id", "newParaElement");

-		widget.select(para);

-		widget.deleteSelection();

-

-		widget.undo(); // delete

-		widget.undo(); // set attribute

-

-		assertXmlEquals(expectedXml, widget);

-	}

-

-	@Test

-	public void whenReadOnly_cannotMorph() throws Exception {

-		widget.insertElement(TITLE);

-		widget.setReadOnly(true);

-		assertFalse(widget.canMorph(PARA));

-	}

-

-	@Test(expected = ReadOnlyException.class)

-	public void whenReadOnly_shouldNotMorph() throws Exception {

-		widget.insertElement(TITLE);

-		widget.setReadOnly(true);

-		widget.morph(PARA);

-	}

-

-	@Test

-	public void cannotMorphRootElement() throws Exception {

-		assertFalse(widget.canMorph(TITLE));

-	}

-

-	@Test

-	public void morphEmptyElement() throws Exception {

-		widget.insertElement(TITLE);

-

-		assertTrue(widget.canMorph(PARA));

-		assertCanMorphOnlyTo(widget, PARA);

-		widget.morph(PARA);

-	}

-

-	@Test

-	public void givenElementWithText_whenMorphing_shouldPreserveText() throws Exception {

-		widget.insertElement(TITLE);

-		widget.insertText("text");

-

-		assertTrue(widget.canMorph(PARA));

-		widget.morph(PARA);

-

-		widget.selectAll();

-		assertXmlEquals("<section><para>text</para></section>", widget);

-	}

-

-	@Test

-	public void givenElementWithChildren_whenStructureIsInvalidAfterMorphing_cannotMorph() throws Exception {

-		widget.insertElement(PARA);

-		widget.insertText("before");

-		widget.insertElement(PRE);

-		widget.insertText("within");

-		widget.moveBy(1);

-		widget.insertText("after");

-

-		assertFalse(widget.canMorph(TITLE));

-	}

-

-	@Test

-	public void givenElementWithChildren_whenStructureIsInvalidAfterMorphing_shouldNotProvideElementToMorph() throws Exception {

-		widget.insertElement(PARA);

-		widget.insertText("before");

-		widget.insertElement(PRE);

-		widget.insertText("within");

-		widget.moveBy(1);

-		widget.insertText("after");

-

-		assertCanMorphOnlyTo(widget /* nothing */);

-	}

-

-	@Test(expected = CannotRedoException.class)

-	public void givenElementWithChildren_whenStructureIsInvalidAfterMorphing_shouldNotMorph() throws Exception {

-		widget.insertElement(PARA);

-		widget.insertText("before");

-		widget.insertElement(PRE);

-		widget.insertText("within");

-		widget.moveBy(1);

-		widget.insertText("after");

-

-		widget.morph(TITLE);

-	}

-

-	@Test

-	public void givenAlternativeElement_whenElementIsNotAllowedAtCurrentInsertionPosition_cannotMorph() throws Exception {

-		widget.insertElement(PARA);

-		widget.moveBy(1);

-		widget.insertElement(PARA);

-

-		assertFalse(widget.canMorph(TITLE));

-	}

-

-	@Test

-	public void givenAlternativeElement_whenElementIsNotAllowedAtCurrentInsertionPosition_shouldNotProvideElementToMorph() throws Exception {

-		widget.insertElement(PARA);

-		widget.moveBy(1);

-		widget.insertElement(PARA);

-

-		assertCanMorphOnlyTo(widget /* nothing */);

-	}

-

-	public void givenElementWithAttributes_whenUndoMorph_shouldPreserveAttributes() throws Exception {

-		widget.insertElement(PARA);

-		widget.setAttribute("id", "idValue");

-		widget.morph(TITLE);

-		widget.undo();

-

-		assertEquals("idValue", widget.getCurrentElement().getAttribute("id").getValue());

-	}

-

-	public void whenReadOnly_cannotUnwrap() throws Exception {

-		widget.insertElement(PARA);

-		widget.insertElement(PRE);

-		widget.insertText("text");

-		widget.setReadOnly(true);

-		assertFalse(widget.canUnwrap());

-	}

-

-	@Test(expected = ReadOnlyException.class)

-	public void whenReadOnly_shouldNotUnwrap() throws Exception {

-		widget.insertElement(PARA);

-		widget.insertElement(PRE);

-		widget.insertText("text");

-		widget.setReadOnly(true);

-		widget.unwrap();

-	}

-

-	@Test

-	public void cannotUnwrapRootElement() throws Exception {

-		assertFalse(widget.canUnwrap());

-	}

-

-	@Test

-	public void unwrapEmptyElement() throws Exception {

-		widget.insertElement(PARA);

-		widget.unwrap();

-

-		widget.selectAll();

-		assertXmlEquals("<section></section>", widget);

-	}

-

-	@Test

-	public void givenInlineElementWithText_shouldUnwrapInlineElement() throws Exception {

-		final IElement para = widget.insertElement(PARA);

-		widget.insertElement(PRE);

-		widget.insertText("text");

-		widget.unwrap();

-

-		assertSame(para, widget.getCurrentElement());

-		widget.selectAll();

-		assertXmlEquals("<section><para>text</para></section>", widget);

-	}

-

-	@Test

-	public void givenElementWithText_whenParentDoesNotAllowText_cannotUnwrap() throws Exception {

-		widget.insertElement(PARA);

-		widget.insertText("text");

-

-		assertFalse(widget.canUnwrap());

-	}

-

-	@Test(expected = CannotRedoException.class)

-	public void givenElementWithText_whenParentDoesNotAllowText_shouldNotUnwrap() throws Exception {

-		widget.insertElement(PARA);

-		widget.insertText("text");

-		widget.unwrap();

-	}

-

-	@Test

-	public void givenElementWithChildren_whenParentDoesNotAllowChildren_cannotUnwrap() throws Exception {

-		widget.insertElement(PARA);

-		widget.insertElement(PRE);

-		widget.moveBy(1);

-

-		assertFalse(widget.canUnwrap());

-	}

-

-	@Test(expected = CannotRedoException.class)

-	public void givenElementWithChildren_whenParentDoesNotAllowChildren_shouldNotUnwrap() throws Exception {

-		widget.insertElement(PARA);

-		widget.insertElement(PRE);

-		widget.moveBy(1);

-		widget.unwrap();

-	}

-

-	@Test

-	public void givenElementWithAttributes_whenUndoUnwrap_shouldPreserveAttributes() throws Exception {

-		widget.insertElement(PARA);

-		widget.insertElement(PRE);

-		widget.setAttribute("id", "idValue");

-

-		widget.unwrap();

-		widget.undo();

-

-		assertEquals(PRE, widget.getCurrentElement().getQualifiedName());

-		assertEquals("idValue", widget.getCurrentElement().getAttributeValue("id"));

-	}

-

-	@Test

-	public void givenMultipleElementsOfSameTypeSelected_canJoin() throws Exception {

-		final IElement firstPara = widget.insertElement(PARA);

-		widget.insertText("1");

-		widget.moveBy(1);

-		widget.insertElement(PARA);

-		widget.insertText("2");

-		widget.moveBy(1);

-		final IElement lastPara = widget.insertElement(PARA);

-		widget.insertText("3");

-		widget.moveBy(1);

-

-		widget.moveTo(firstPara.getStartPosition());

-		widget.moveTo(lastPara.getEndPosition(), true);

-

-		assertTrue(widget.canJoin());

-	}

-

-	@Test

-	public void givenMultipleElementsOfSameTypeSelected_shouldJoin() throws Exception {

-		final IElement firstPara = widget.insertElement(PARA);

-		widget.insertText("1");

-		widget.moveBy(1);

-		widget.insertElement(PARA);

-		widget.insertText("2");

-		widget.moveBy(1);

-		final IElement lastPara = widget.insertElement(PARA);

-		widget.insertText("3");

-		widget.moveBy(1);

-

-		widget.moveTo(firstPara.getStartPosition());

-		widget.moveTo(lastPara.getEndPosition(), true);

-

-		widget.join();

-

-		assertXmlEquals("<section><para>123</para></section>", widget);

-	}

-

-	@Test

-	public void givenMultipleElementsOfSameKindSelected_whenJoining_shouldPreserveAttributesOfFirstElement() throws Exception {

-		final IElement firstPara = widget.insertElement(PARA);

-		widget.insertText("1");

-		widget.moveBy(1);

-		widget.insertElement(PARA);

-		widget.insertText("2");

-		widget.moveBy(1);

-		final IElement lastPara = widget.insertElement(PARA);

-		widget.insertText("3");

-		firstPara.setAttribute("id", "para1");

-		lastPara.setAttribute("id", "para3");

-

-		widget.moveTo(firstPara.getStartPosition());

-		widget.moveTo(lastPara.getEndPosition(), true);

-

-		widget.join();

-

-		assertXmlEquals("<section><para id=\"para1\">123</para></section>", widget);

-	}

-

-	@Test

-	public void givenMultipleElementsOfSameKindSelected_whenJoinUndone_shouldRestoreAttributesOfAllElements() throws Exception {

-		final IElement firstPara = widget.insertElement(PARA);

-		widget.insertText("1");

-		widget.moveBy(1);

-		widget.insertElement(PARA);

-		widget.insertText("2");

-		widget.moveBy(1);

-		final IElement lastPara = widget.insertElement(PARA);

-		widget.insertText("3");

-		firstPara.setAttribute("id", "para1");

-		lastPara.setAttribute("id", "para3");

-

-		widget.moveTo(firstPara.getStartPosition());

-		widget.moveTo(lastPara.getEndPosition(), true);

-

-		widget.join();

-		widget.undo();

-

-		assertXmlEquals("<section><para id=\"para1\">1</para><para>2</para><para id=\"para3\">3</para></section>", widget);

-	}

-

-	@Test

-	public void givenMultipleElementsOfSameKindSelected_whenStructureAfterJoinWouldBeInvalid_cannotJoin() throws Exception {

-		widget.setDocument(createDocumentWithDTD(TEST_DTD, "one-kind-of-child"), readTestStyleSheet());

-		rootElement = widget.getDocument().getRootElement();

-

-		final IElement firstSection = widget.insertElement(SECTION);

-		widget.insertElement(TITLE);

-		widget.moveBy(2);

-		widget.insertElement(SECTION);

-		widget.insertElement(TITLE);

-		widget.moveBy(2);

-		final IElement lastSection = widget.insertElement(SECTION);

-		widget.insertElement(TITLE);

-

-		widget.moveTo(firstSection.getStartPosition());

-		widget.moveTo(lastSection.getEndPosition(), true);

-

-		assertFalse(widget.canJoin());

-	}

-

-	@Test(expected = CannotRedoException.class)

-	public void givenMultipleElementsOfSameKindSelected_whenStructureAfterJoinWouldBeInvalid_shouldJoin() throws Exception {

-		widget.setDocument(createDocumentWithDTD(TEST_DTD, "one-kind-of-child"), readTestStyleSheet());

-		rootElement = widget.getDocument().getRootElement();

-

-		final IElement firstSection = widget.insertElement(SECTION);

-		widget.insertElement(TITLE);

-		widget.moveBy(2);

-		widget.insertElement(SECTION);

-		widget.insertElement(TITLE);

-		widget.moveBy(2);

-		final IElement lastSection = widget.insertElement(SECTION);

-		widget.insertElement(TITLE);

-

-		widget.moveTo(firstSection.getStartPosition());

-		widget.moveTo(lastSection.getEndPosition(), true);

-

-		widget.join();

-	}

-

-	@Test

-	public void givenMultipleElementsOfDifferentTypeSelected_cannotJoin() throws Exception {

-		final IElement title = widget.insertElement(TITLE);

-		widget.insertText("1");

-		widget.moveBy(1);

-		widget.insertElement(PARA);

-		widget.insertText("2");

-		widget.moveBy(1);

-		final IElement lastPara = widget.insertElement(PARA);

-		widget.insertText("3");

-		widget.moveBy(1);

-

-		widget.moveTo(title.getStartPosition());

-		widget.moveTo(lastPara.getEndPosition(), true);

-

-		assertFalse(widget.canJoin());

-	}

-

-	@Test(expected = DocumentValidationException.class)

-	public void givenMultipleElementsOfDifferentTypeSelected_shouldNotJoin() throws Exception {

-		final IElement title = widget.insertElement(TITLE);

-		widget.insertText("1");

-		widget.moveBy(1);

-		widget.insertElement(PARA);

-		widget.insertText("2");

-		widget.moveBy(1);

-		final IElement lastPara = widget.insertElement(PARA);

-		widget.insertText("3");

-		widget.moveBy(1);

-

-		widget.moveTo(title.getStartPosition());

-		widget.moveTo(lastPara.getEndPosition(), true);

-

-		widget.join();

-	}

-

-	@Test

-	public void givenSelectionIsEmpty_cannotJoin() throws Exception {

-		assertFalse(widget.canJoin());

-	}

-

-	@Test

-	public void givenSelectionIsEmpty_whenRequestedToJoin_shouldIgnoreGracefully() throws Exception {

-		final String expectedXml = getCurrentXML(widget);

-

-		widget.join();

-

-		assertXmlEquals(expectedXml, widget);

-	}

-

-	@Test

-	public void givenOnlyTextSelected_cannotJoin() throws Exception {

-		final IElement title = widget.insertElement(TITLE);

-		widget.insertText("title text");

-		widget.moveTo(title.getStartPosition().moveBy(1), true);

-

-		assertFalse(widget.canJoin());

-	}

-

-	@Test

-	public void givenOnlyTextSelected_whenRequestedToJoin_shouldIgnoreGracefully() throws Exception {

-		final IElement title = widget.insertElement(TITLE);

-		widget.insertText("title text");

-		widget.selectContentOf(title);

-		final String expectedXml = getCurrentXML(widget);

-

-		widget.join();

-

-		assertXmlEquals(expectedXml, widget);

-	}

-

-	@Test

-	public void givenOnlySingleElementSelected_cannotJoin() throws Exception {

-		final IElement title = widget.insertElement(TITLE);

-		widget.moveTo(title.getStartPosition(), true);

-

-		assertFalse(widget.canJoin());

-	}

-

-	@Test

-	public void givenOnlySingleElementSelected_whenRequestedToJoin_shouldIgnoreGracefully() throws Exception {

-		final IElement title = widget.insertElement(TITLE);

-		widget.moveTo(title.getStartPosition(), true);

-		final String expectedXml = getCurrentXML(widget);

-

-		widget.join();

-

-		assertXmlEquals(expectedXml, widget);

-	}

-

-	@Test

-	public void givenMultipleCommentsSelected_canJoin() throws Exception {

-		final IComment firstComment = widget.insertComment();

-		widget.insertText("comment1");

-		widget.moveBy(1);

-		widget.insertComment();

-		widget.insertText("comment2");

-		widget.moveBy(1);

-		final IComment lastComment = widget.insertComment();

-		widget.insertText("comment3");

-

-		widget.moveTo(firstComment.getStartPosition());

-		widget.moveTo(lastComment.getEndPosition(), true);

-

-		assertTrue(widget.canJoin());

-	}

-

-	@Test

-	public void givenMultipleCommentsSelected_shouldJoin() throws Exception {

-		final IComment firstComment = widget.insertComment();

-		widget.insertText("comment1");

-		widget.moveBy(1);

-		widget.insertComment();

-		widget.insertText("comment2");

-		widget.moveBy(1);

-		final IComment lastComment = widget.insertComment();

-		widget.insertText("comment3");

-

-		widget.moveTo(firstComment.getStartPosition());

-		widget.moveTo(lastComment.getEndPosition(), true);

-

-		widget.join();

-

-		assertXmlEquals("<section><!--comment1comment2comment3--></section>", widget);

-	}

-

-	@Test

-	public void givenMultipleInlineElementsOfSameKindSelected_whenTextEndsWithSpace_shouldJoin() throws Exception {

-		widget.insertElement(PARA);

-		final IElement firstElement = widget.insertElement(PRE);

-		widget.insertText("1");

-		widget.moveBy(1);

-		final IElement lastElement = widget.insertElement(PRE);

-		widget.insertText("2 ");

-

-		widget.moveTo(firstElement.getStartPosition());

-		widget.moveTo(lastElement.getEndPosition(), true);

-

-		widget.join();

-

-		assertXmlEquals("<section><para><pre>12 </pre></para></section>", widget);

-	}

-

-	@Test

-	public void givenMultipleInlineElementsOfSameKindSelected_whenTextBetweenElements_cannotJoin() throws Exception {

-		widget.insertElement(PARA);

-		final IElement firstElement = widget.insertElement(PRE);

-		widget.insertText("1");

-		widget.moveBy(1);

-		widget.insertText("text between elements");

-		final IElement lastElement = widget.insertElement(PRE);

-		widget.insertText("2 ");

-

-		widget.moveTo(firstElement.getStartPosition());

-		widget.moveTo(lastElement.getEndPosition(), true);

-

-		assertFalse(widget.canJoin());

-	}

-

-	@Test(expected = DocumentValidationException.class)

-	public void givenMultipleInlineElementsOfSameKindSelected_whenTextBetweenElements_shouldNotJoin() throws Exception {

-		widget.insertElement(PARA);

-		final IElement firstElement = widget.insertElement(PRE);

-		widget.insertText("1");

-		widget.moveBy(1);

-		widget.insertText("text between elements");

-		final IElement lastElement = widget.insertElement(PRE);

-		widget.insertText("2 ");

-

-		widget.moveTo(firstElement.getStartPosition());

-		widget.moveTo(lastElement.getEndPosition(), true);

-

-		widget.join();

-	}

-

-	@Test

-	public void givenDeletedText_whenDeleteUndone_shouldSetCaretToEndOfRecoveredText() throws Exception {

-		final IElement title = widget.insertElement(TITLE);

-		widget.insertText("Hello World");

-		widget.moveTo(title.getStartPosition().moveBy(1));

-		widget.moveBy(5, true);

-		final int expectedCaretPosition = widget.getSelectedRange().getEndOffset() + 1;

-

-		widget.deleteSelection();

-		widget.undo();

-

-		assertEquals(expectedCaretPosition, widget.getCaretPosition().getOffset());

-	}

-

-	@Test

-	public void afterDeletingSelection_CaretPositionShouldBeValid() throws Exception {

-		final IElement para = widget.insertElement(PARA);

-		widget.moveTo(para.getEndPosition());

-		final IElement pre = widget.insertElement(PRE);

-		widget.insertText("Hello World");

-		widget.moveTo(pre.getStartPosition());

-		widget.moveTo(pre.getEndPosition().moveBy(-1), true);

-

-		widget.deleteSelection();

-		assertEquals(para.getEndPosition(), widget.getCaretPosition());

-	}

-

-	@Test

-	public void undoAndRedoDelete() throws Exception {

-		final IElement para = widget.insertElement(PARA);

-		widget.moveTo(para.getEndPosition());

-		final IElement pre = widget.insertElement(PRE);

-		widget.insertText("Hello World");

-		final String beforeDeleteXml = getCurrentXML(widget);

-

-		widget.moveTo(pre.getStartPosition());

-		widget.moveTo(pre.getEndPosition().moveBy(-1), true);

-		widget.deleteSelection();

-

-		final String beforeUndoXml = getCurrentXML(widget);

-		widget.undo();

-		assertXmlEquals(beforeDeleteXml, widget);

-

-		widget.redo();

-		assertXmlEquals(beforeUndoXml, widget);

-	}

-

-	private static StyleSheet readTestStyleSheet() throws IOException {

-		return new StyleSheetReader().read(TestResources.get("test.css"));

-	}

-

-}

+/*******************************************************************************
+ * Copyright (c) 2012, 2013 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
+ * 		Carsten Hiesserich - additional tests (bug 407827, 409032)
+ *******************************************************************************/
+package org.eclipse.vex.core.internal.widget;
+
+import static org.eclipse.vex.core.internal.widget.VexWidgetTest.PARA;
+import static org.eclipse.vex.core.internal.widget.VexWidgetTest.PRE;
+import static org.eclipse.vex.core.internal.widget.VexWidgetTest.SECTION;
+import static org.eclipse.vex.core.internal.widget.VexWidgetTest.TITLE;
+import static org.eclipse.vex.core.internal.widget.VexWidgetTest.assertCanMorphOnlyTo;
+import static org.eclipse.vex.core.internal.widget.VexWidgetTest.assertXmlEquals;
+import static org.eclipse.vex.core.internal.widget.VexWidgetTest.createDocumentWithDTD;
+import static org.eclipse.vex.core.internal.widget.VexWidgetTest.getCurrentXML;
+import static org.eclipse.vex.core.tests.TestResources.TEST_DTD;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.util.List;
+
+import org.eclipse.vex.core.internal.css.CssWhitespacePolicy;
+import org.eclipse.vex.core.internal.css.StyleSheet;
+import org.eclipse.vex.core.internal.css.StyleSheetReader;
+import org.eclipse.vex.core.internal.undo.CannotApplyException;
+import org.eclipse.vex.core.provisional.dom.DocumentValidationException;
+import org.eclipse.vex.core.provisional.dom.IComment;
+import org.eclipse.vex.core.provisional.dom.IDocument;
+import org.eclipse.vex.core.provisional.dom.IDocumentFragment;
+import org.eclipse.vex.core.provisional.dom.IElement;
+import org.eclipse.vex.core.provisional.dom.INode;
+import org.eclipse.vex.core.provisional.dom.IParent;
+import org.eclipse.vex.core.provisional.dom.IText;
+import org.eclipse.vex.core.tests.TestResources;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author Florian Thienel
+ */
+public class L2SimpleEditingTest {
+
+	private FakeCursor cursor;
+	private IDocumentEditor editor;
+	private IElement rootElement;
+
+	@Before
+	public void setUp() throws Exception {
+		final IDocument document = createDocumentWithDTD(TEST_DTD, "section");
+		cursor = new FakeCursor(document);
+		editor = new DocumentEditor(cursor, new CssWhitespacePolicy(readTestStyleSheet()));
+		editor.setDocument(document);
+		rootElement = editor.getDocument().getRootElement();
+	}
+
+	private void useDocument(final IDocument document) {
+		cursor.setDocument(document);
+		editor.setDocument(document);
+		rootElement = editor.getDocument().getRootElement();
+	}
+
+	@Test
+	public void shouldStartInRootElement() throws Exception {
+		assertSame(rootElement, editor.getCurrentElement());
+		assertEquals(rootElement.getEndPosition(), editor.getCaretPosition());
+	}
+
+	@Test
+	public void shouldMoveCaretIntoInsertedElement() throws Exception {
+		final IElement titleElement = editor.insertElement(TITLE);
+		assertEquals(titleElement.getEndPosition(), editor.getCaretPosition());
+	}
+
+	@Test
+	public void shouldProvideInsertionElementAsCurrentElement() throws Exception {
+		final IElement titleElement = editor.insertElement(TITLE);
+		editor.moveBy(-1);
+		assertEquals(titleElement.getStartPosition().getOffset(), editor.getCaretPosition().getOffset());
+		assertSame(rootElement, editor.getCurrentElement());
+	}
+
+	@Test
+	public void givenAnElementWithText_whenAtEndOfTextAndHittingBackspace_shouldDeleteLastCharacter() throws Exception {
+		final IElement titleElement = editor.insertElement(TITLE);
+		editor.insertText("Hello");
+		editor.deleteBackward();
+		assertEquals("Hell", titleElement.getText());
+		assertEquals(titleElement.getEndPosition(), editor.getCaretPosition());
+	}
+
+	@Test
+	public void givenAnElementWithText_whenAtBeginningOfTextAndHittingDelete_shouldDeleteFirstCharacter() throws Exception {
+		final IElement titleElement = editor.insertElement(TITLE);
+		editor.insertText("Hello");
+		editor.moveBy(-5);
+		editor.deleteForward();
+		assertEquals("ello", titleElement.getText());
+		assertEquals(titleElement.getStartPosition().moveBy(1), editor.getCaretPosition());
+	}
+
+	@Test
+	public void givenAnEmptyElement_whenCaretBetweenStartAndEndTagAndHittingBackspace_shouldDeleteEmptyElement() throws Exception {
+		editor.insertElement(TITLE);
+		editor.moveBy(1);
+		final IElement paraElement = editor.insertElement(PARA);
+		editor.deleteBackward();
+		assertEquals(1, rootElement.children().count());
+		assertNull(paraElement.getParent());
+		assertFalse(paraElement.isAssociated());
+	}
+
+	@Test
+	public void givenAnEmptyElement_whenCaretBetweenStartAndEndTagAndHittingDelete_shouldDeleteEmptyElement() throws Exception {
+		editor.insertElement(TITLE);
+		editor.moveBy(1);
+		final IElement paraElement = editor.insertElement(PARA);
+		editor.deleteForward();
+		assertEquals(1, rootElement.children().count());
+		assertNull(paraElement.getParent());
+		assertFalse(paraElement.isAssociated());
+	}
+
+	@Test
+	public void givenAnEmptyElement_whenCaretAfterEndTagAndHittingDelete_shouldDeleteEmptyElement() throws Exception {
+		editor.insertElement(TITLE);
+		editor.moveBy(1);
+		final IElement paraElement = editor.insertElement(PARA);
+		editor.moveBy(1);
+		editor.deleteBackward();
+		assertEquals(1, rootElement.children().count());
+		assertNull(paraElement.getParent());
+		assertFalse(paraElement.isAssociated());
+	}
+
+	@Test
+	public void givenAnEmptyElement_whenCaretBeforeStartTagAndHittingDelete_shouldDeleteEmptyElement() throws Exception {
+		editor.insertElement(TITLE);
+		editor.moveBy(1);
+		final IElement paraElement = editor.insertElement(PARA);
+		editor.moveBy(-1);
+		editor.deleteForward();
+		assertEquals(1, rootElement.children().count());
+		assertNull(paraElement.getParent());
+		assertFalse(paraElement.isAssociated());
+	}
+
+	@Test
+	public void givenTwoMatchingElements_whenCaretBetweenEndAndStartTagAndHittingBackspace_shouldJoinElements() throws Exception {
+		editor.insertElement(TITLE);
+		editor.moveBy(1);
+		final IElement para1 = editor.insertElement(PARA);
+		editor.insertText("Hello");
+		editor.moveBy(1);
+		final IElement para2 = editor.insertElement(PARA);
+		editor.insertText("World");
+
+		editor.moveTo(para2.getStartPosition());
+		editor.deleteBackward();
+
+		assertEquals(2, rootElement.children().count());
+		assertSame(rootElement, para1.getParent());
+		assertTrue(para1.isAssociated());
+		assertEquals("HelloWorld", para1.getText());
+		assertNull(para2.getParent());
+		assertFalse(para2.isAssociated());
+	}
+
+	@Test
+	public void givenTwoMatchingElements_whenCaretBetweenEndAndStartTagAndHittingDelete_shouldJoinElements() throws Exception {
+		editor.insertElement(TITLE);
+		editor.moveBy(1);
+		final IElement para1 = editor.insertElement(PARA);
+		editor.insertText("Hello");
+		editor.moveBy(1);
+		final IElement para2 = editor.insertElement(PARA);
+		editor.insertText("World");
+
+		editor.moveTo(para2.getStartPosition());
+		editor.deleteForward();
+
+		assertEquals(2, rootElement.children().count());
+		assertSame(rootElement, para1.getParent());
+		assertTrue(para1.isAssociated());
+		assertEquals("HelloWorld", para1.getText());
+		assertNull(para2.getParent());
+		assertFalse(para2.isAssociated());
+	}
+
+	@Test
+	public void givenTwoMatchingElements_whenCaretAfterStartTagOfSecondElementAndHittingBackspace_shouldJoinElements() throws Exception {
+		editor.insertElement(TITLE);
+		editor.moveBy(1);
+		final IElement para1 = editor.insertElement(PARA);
+		editor.insertText("Hello");
+		editor.moveBy(1);
+		final IElement para2 = editor.insertElement(PARA);
+		editor.insertText("World");
+
+		editor.moveTo(para2.getStartPosition().moveBy(1));
+		editor.deleteBackward();
+
+		assertEquals(2, rootElement.children().count());
+		assertSame(rootElement, para1.getParent());
+		assertTrue(para1.isAssociated());
+		assertEquals("HelloWorld", para1.getText());
+		assertNull(para2.getParent());
+		assertFalse(para2.isAssociated());
+	}
+
+	@Test
+	public void givenTwoMatchingElements_whenCaretBeforeEndTagOfFirstElementAndHittingDelete_shouldJoinElements() throws Exception {
+		editor.insertElement(TITLE);
+		editor.moveBy(1);
+		final IElement para1 = editor.insertElement(PARA);
+		editor.insertText("Hello");
+		editor.moveBy(1);
+		final IElement para2 = editor.insertElement(PARA);
+		editor.insertText("World");
+
+		editor.moveTo(para1.getEndPosition());
+		editor.deleteForward();
+
+		assertEquals(2, rootElement.children().count());
+		assertSame(rootElement, para1.getParent());
+		assertTrue(para1.isAssociated());
+		assertEquals("HelloWorld", para1.getText());
+		assertNull(para2.getParent());
+		assertFalse(para2.isAssociated());
+	}
+
+	@Test
+	public void givenElementWithText_whenAllTextSelectedAndInsertingACharacter_shouldReplaceAllTextWithNewCharacter() throws Exception {
+		final IElement titleElement = editor.insertElement(TITLE);
+		editor.insertText("Hello World");
+
+		editor.selectContentOf(titleElement);
+		editor.insertChar('A');
+
+		assertEquals("A", titleElement.getText());
+	}
+
+	@Test(expected = ReadOnlyException.class)
+	public void whenReadOnly_shouldNotInsertText() throws Exception {
+		editor.insertElement(TITLE); // need an element where text would be valid
+		editor.setReadOnly(true);
+		assertFalse(editor.canInsertText());
+		editor.insertText("Hello World");
+	}
+
+	@Test(expected = ReadOnlyException.class)
+	public void whenReadOnly_shouldNotInsertChar() throws Exception {
+		editor.insertElement(TITLE); // need an element where text would be valid
+		editor.setReadOnly(true);
+		assertFalse(editor.canInsertText());
+		editor.insertChar('H');
+	}
+
+	@Test(expected = ReadOnlyException.class)
+	public void whenReadOnly_shouldNotInsertElement() throws Exception {
+		editor.setReadOnly(true);
+		assertTrue(editor.getValidInsertElements().length == 0);
+		editor.insertElement(TITLE);
+	}
+
+	@Test(expected = ReadOnlyException.class)
+	public void whenReadOnly_shouldNotInsertComment() throws Exception {
+		editor.setReadOnly(true);
+		assertFalse(editor.canInsertComment());
+		editor.insertComment();
+	}
+
+	@Test(expected = ReadOnlyException.class)
+	public void whenReadOnly_shouldNotInsertFragment() throws Exception {
+		editor.insertElement(TITLE);
+		editor.moveBy(1);
+		final IElement para = editor.insertElement(PARA);
+		final IDocumentFragment fragment = editor.getDocument().getFragment(para.getRange());
+		editor.moveTo(rootElement.getEndPosition());
+
+		editor.setReadOnly(true);
+		assertFalse(editor.canInsertFragment(fragment));
+		editor.insertFragment(fragment);
+	}
+
+	@Test
+	public void givenNonPreElement_whenInsertingNewline_shouldSplitElement() throws Exception {
+		editor.insertElement(TITLE);
+		editor.moveBy(1);
+		final IElement para1 = editor.insertElement(PARA);
+		editor.insertText("Hello");
+		editor.moveTo(para1.getEndPosition());
+		editor.insertText("\n");
+		assertEquals("Hello", para1.getText());
+		assertEquals(3, rootElement.children().count());
+	}
+
+	@Test
+	public void givenPreElement_whenInsertingNewline_shouldInsertNewline() throws Exception {
+		final IElement para = editor.insertElement(PARA);
+		final IElement preElement = editor.insertElement(PRE);
+		assertEquals(preElement.getEndPosition(), editor.getCaretPosition());
+		editor.insertText("line1");
+		editor.insertText("\n");
+		assertEquals(1, para.children().count());
+		assertEquals("line1\n", preElement.getText());
+	}
+
+	@Test
+	public void insertingTextAtInvalidPosition_shouldNotAlterDocument() throws Exception {
+		final IElement para1 = editor.insertElement(PARA);
+		editor.moveBy(1);
+		final IElement para2 = editor.insertElement(PARA);
+		editor.moveTo(para1.getEndPosition());
+		editor.insertText("Para1");
+		editor.moveTo(para2.getEndPosition());
+		editor.insertText("Para2");
+
+		// Insert position is invalid
+		editor.moveTo(para1.getEndPosition().moveBy(1));
+		try {
+			editor.insertText("Test");
+		} catch (final Exception ex) {
+		} finally {
+			assertEquals("Para1", para1.getText());
+			assertEquals("Para2", para2.getText());
+		}
+	}
+
+	@Test
+	public void insertingElementAtInvalidPosition_shouldNotAlterDocument() throws Exception {
+		final IElement para1 = editor.insertElement(PARA);
+		editor.moveBy(1);
+		final IElement para2 = editor.insertElement(PARA);
+		editor.moveTo(para1.getEndPosition());
+		editor.insertText("Para1");
+		editor.moveTo(para2.getEndPosition());
+		editor.insertText("Para2");
+
+		// Insert position is invalid
+		editor.moveTo(para1.getEndPosition());
+		try {
+			editor.insertElement(PARA);
+		} catch (final Exception ex) {
+		} finally {
+			assertEquals("Para1", para1.getText());
+			assertEquals("Para2", para2.getText());
+		}
+	}
+
+	@Test
+	public void givenPreElement_whenInsertingTextWithNewline_shouldInsertNewline() throws Exception {
+		editor.insertElement(PARA);
+		final IElement preElement = editor.insertElement(PRE);
+		assertEquals(preElement.getEndPosition(), editor.getCaretPosition());
+		editor.insertText("line1\nline2");
+		assertEquals("line1\nline2", preElement.getText());
+	}
+
+	@Test
+	public void givenPreElement_whenInsertingText_shouldKeepWhitespace() throws Exception {
+		editor.insertElement(PARA);
+		final IElement preElement = editor.insertElement(PRE);
+
+		editor.moveTo(preElement.getEndPosition());
+		editor.insertText("line1\nline2   end");
+
+		final List<? extends INode> children = preElement.children().asList();
+		assertTrue("Expecting IText", children.get(0) instanceof IText);
+		assertEquals("line1\nline2   end", children.get(0).getText());
+	}
+
+	@Test
+	public void givenNonPreElement_whenInsertingText_shouldCompressWhitespace() throws Exception {
+		final IElement para = editor.insertElement(PARA);
+		editor.moveTo(para.getEndPosition());
+		editor.insertText("line1\nline2   \t end");
+
+		final List<? extends INode> children = rootElement.children().after(para.getStartPosition().getOffset()).asList();
+		assertEquals("two para elements", 2, children.size());
+		assertTrue("original para element", children.get(0) instanceof IParent);
+		assertTrue("splitted para element", children.get(1) instanceof IParent);
+		assertEquals("first line", "line1", children.get(0).getText());
+		assertEquals("second line with compressed whitespace", "line2 end", children.get(1).getText());
+	}
+
+	@Test
+	public void givenNonPreElement_whenInsertingText_shouldCompressNewlines() throws Exception {
+		final IElement para = editor.insertElement(PARA);
+		editor.moveTo(para.getEndPosition());
+		editor.insertText("line1\n\nline2");
+
+		final List<? extends INode> children = rootElement.children().after(para.getStartPosition().getOffset()).asList();
+		assertEquals("two para elements", 2, children.size());
+		assertTrue("original para element", children.get(0) instanceof IParent);
+		assertTrue("splitted para element", children.get(1) instanceof IParent);
+		assertEquals("first line", "line1", children.get(0).getText());
+		assertEquals("second line", "line2", children.get(1).getText());
+	}
+
+	@Test
+	public void givenNonPreElement_whenSplitting_shouldSplitIntoTwoElements() throws Exception {
+		final IElement para = editor.insertElement(PARA);
+		editor.moveTo(para.getEndPosition());
+		editor.insertText("line1line2");
+		editor.moveBy(-5);
+
+		editor.split();
+
+		final List<? extends INode> children = rootElement.children().after(para.getStartPosition().getOffset()).asList();
+		assertEquals("two para elements", 2, children.size());
+		assertTrue("original para element", children.get(0) instanceof IParent);
+		assertTrue("splitted para element", children.get(1) instanceof IParent);
+		assertEquals("first line element", PARA, ((IElement) children.get(0)).getQualifiedName());
+		assertEquals("second line element", PARA, ((IElement) children.get(0)).getQualifiedName());
+		assertEquals("first line", "line1", children.get(0).getText());
+		assertEquals("second line", "line2", children.get(1).getText());
+	}
+
+	@Test
+	public void givenPreElement_whenSplitting_shouldSplitIntoTwoElements() throws Exception {
+		final IElement para = editor.insertElement(PARA);
+		final IElement preElement = editor.insertElement(PRE);
+		editor.moveTo(preElement.getEndPosition());
+		editor.insertText("line1line2");
+		editor.moveBy(-5);
+
+		editor.split();
+
+		final List<? extends INode> children = para.children().after(preElement.getStartPosition().getOffset()).asList();
+		assertEquals("two para elements", 2, children.size());
+		assertTrue("original pre element", children.get(0) instanceof IParent);
+		assertTrue("splitted pre element", children.get(1) instanceof IParent);
+		assertEquals("first line element", PRE, ((IElement) children.get(0)).getQualifiedName());
+		assertEquals("second line element", PRE, ((IElement) children.get(0)).getQualifiedName());
+		assertEquals("first line", "line1", children.get(0).getText());
+		assertEquals("second line", "line2", children.get(1).getText());
+	}
+
+	@Test
+	public void givenElementWithMultipleOccurrence_canSplitElement() throws Exception {
+		editor.insertElement(PARA);
+		assertTrue(editor.canSplit());
+	}
+
+	@Test
+	public void givenElementWithMultipleOccurrence_whenAnythingIsSelected_canSplitElement() throws Exception {
+		editor.insertElement(PARA);
+		editor.insertText("12345");
+		editor.moveBy(-3, true);
+		assertTrue(editor.canSplit());
+	}
+
+	@Test
+	public void givenElementWithMultipleOccurrence_whenSplittingWithAnythingSelected_shouldDeleteSelectionAndSplit() throws Exception {
+		editor.insertElement(PARA);
+		editor.insertText("12345");
+		editor.moveBy(-3, true);
+		editor.split();
+
+		final List<? extends INode> children = rootElement.children().asList();
+		assertEquals("two para elements", 2, children.size());
+		assertTrue("original para element", children.get(0) instanceof IParent);
+		assertTrue("splitted para element", children.get(1) instanceof IParent);
+		assertEquals("first line element", PARA, ((IElement) children.get(0)).getQualifiedName());
+		assertEquals("second line element", PARA, ((IElement) children.get(0)).getQualifiedName());
+		assertEquals("first line", "12", children.get(0).getText());
+		assertEquals("second line", "", children.get(1).getText());
+	}
+
+	@Test
+	public void givenElementWithMultipleOccurrence_whenCaretRightAfterStartIndex_shouldSplit() throws Exception {
+		final IElement para = editor.insertElement(PARA);
+		editor.insertText("12345");
+		editor.moveTo(para.getStartPosition().moveBy(1));
+		editor.split();
+
+		final List<? extends INode> children = rootElement.children().asList();
+		assertEquals("two para elements", 2, children.size());
+		assertTrue("original para element", children.get(0) instanceof IParent);
+		assertTrue("splitted para element", children.get(1) instanceof IParent);
+		assertEquals("first line element", PARA, ((IElement) children.get(0)).getQualifiedName());
+		assertEquals("second line element", PARA, ((IElement) children.get(0)).getQualifiedName());
+		assertEquals("first line", "", children.get(0).getText());
+		assertEquals("second line", "12345", children.get(1).getText());
+	}
+
+	@Test
+	public void givenElementWithMultipleOccurrenceAndInlineElement_whenCaretAtInlineStartOffset_shouldSplit() throws Exception {
+		editor.insertElement(PARA);
+		editor.insertText("12");
+		editor.insertElement(PRE);
+		editor.insertText("34");
+		editor.moveBy(1);
+		editor.insertText("56");
+		editor.moveBy(-6);
+		editor.split();
+
+		final List<? extends INode> children = rootElement.children().asList();
+		assertEquals("two para elements", 2, children.size());
+		assertTrue("original para element", children.get(0) instanceof IParent);
+		assertTrue("splitted para element", children.get(1) instanceof IParent);
+		assertEquals("first line element", PARA, ((IElement) children.get(0)).getQualifiedName());
+		assertEquals("second line element", PARA, ((IElement) children.get(0)).getQualifiedName());
+		assertEquals("first line", "12", children.get(0).getText());
+		assertEquals("second line", "3456", children.get(1).getText());
+		assertEquals("pre in second line", PRE, ((IElement) ((IElement) children.get(1)).children().get(0)).getQualifiedName());
+	}
+
+	@Test
+	public void undoSubsequentSplitsOfInlineAndBlock() throws Exception {
+		editor.insertElement(PARA);
+		editor.insertText("12");
+		editor.insertElement(PRE);
+		editor.insertText("34");
+		editor.moveBy(1);
+		editor.insertText("56");
+		final String expectedXml = getCurrentXML(editor);
+
+		editor.moveBy(-4);
+		editor.split();
+
+		editor.moveBy(-1);
+		editor.split();
+
+		editor.undo();
+		editor.undo();
+
+		assertXmlEquals(expectedXml, editor);
+	}
+
+	@Test
+	public void givenElementWithSingleOccurrence_cannotSplitElement() throws Exception {
+		editor.insertElement(TITLE);
+		assertFalse(editor.canSplit());
+	}
+
+	@Test(expected = CannotApplyException.class)
+	public void givenElementWithSingleOccurrence_whenSplitting_shouldThrowCannotRedoException() throws Exception {
+		editor.insertElement(TITLE);
+		editor.split();
+	}
+
+	@Test
+	public void givenComment_cannotSplit() throws Exception {
+		editor.insertComment();
+		assertFalse(editor.canSplit());
+	}
+
+	@Test(expected = DocumentValidationException.class)
+	public void givenComment_whenSplitting_shouldThrowDocumentValidationException() throws Exception {
+		editor.insertComment();
+		editor.split();
+	}
+
+	@Test
+	public void givenBeforeRootElement_cannotSplitElement() throws Exception {
+		editor.moveTo(rootElement.getStartPosition());
+		assertFalse(editor.canSplit());
+	}
+
+	@Test(expected = DocumentValidationException.class)
+	public void givenBeforeRootElement_whenSplitting_shouldThrowDocumentValidationException() throws Exception {
+		editor.moveTo(rootElement.getStartPosition());
+		editor.split();
+	}
+
+	@Test
+	public void undoRedoChangeNamespaceWithSubsequentDelete() throws Exception {
+		editor.insertElement(TITLE);
+		editor.moveBy(1);
+		final IElement para = editor.insertElement(PARA);
+		final String expectedXml = getCurrentXML(editor);
+
+		editor.declareNamespace("ns1", "nsuri1");
+		editor.select(para);
+		editor.deleteSelection();
+
+		editor.undo(); // delete
+		editor.undo(); // declare namespace
+
+		assertXmlEquals(expectedXml, editor);
+	}
+
+	@Test
+	public void undoRedoChangeAttributeWithSubsequentDelete() throws Exception {
+		editor.insertElement(TITLE);
+		editor.moveBy(1);
+		final IElement para = editor.insertElement(PARA);
+		final String expectedXml = getCurrentXML(editor);
+
+		editor.setAttribute("id", "newParaElement");
+		editor.select(para);
+		editor.deleteSelection();
+
+		editor.undo(); // delete
+		editor.undo(); // set attribute
+
+		assertXmlEquals(expectedXml, editor);
+	}
+
+	@Test
+	public void whenReadOnly_cannotMorph() throws Exception {
+		editor.insertElement(TITLE);
+		editor.setReadOnly(true);
+		assertFalse(editor.canMorph(PARA));
+	}
+
+	@Test(expected = ReadOnlyException.class)
+	public void whenReadOnly_shouldNotMorph() throws Exception {
+		editor.insertElement(TITLE);
+		editor.setReadOnly(true);
+		editor.morph(PARA);
+	}
+
+	@Test
+	public void cannotMorphRootElement() throws Exception {
+		assertFalse(editor.canMorph(TITLE));
+	}
+
+	@Test
+	public void morphEmptyElement() throws Exception {
+		editor.insertElement(TITLE);
+
+		assertTrue(editor.canMorph(PARA));
+		assertCanMorphOnlyTo(editor, PARA);
+		editor.morph(PARA);
+	}
+
+	@Test
+	public void givenElementWithText_whenMorphing_shouldPreserveText() throws Exception {
+		editor.insertElement(TITLE);
+		editor.insertText("text");
+
+		assertTrue(editor.canMorph(PARA));
+		editor.morph(PARA);
+
+		editor.selectAll();
+		assertXmlEquals("<section><para>text</para></section>", editor);
+	}
+
+	@Test
+	public void givenElementWithChildren_whenStructureIsInvalidAfterMorphing_cannotMorph() throws Exception {
+		editor.insertElement(PARA);
+		editor.insertText("before");
+		editor.insertElement(PRE);
+		editor.insertText("within");
+		editor.moveBy(1);
+		editor.insertText("after");
+
+		assertFalse(editor.canMorph(TITLE));
+	}
+
+	@Test
+	public void givenElementWithChildren_whenStructureIsInvalidAfterMorphing_shouldNotProvideElementToMorph() throws Exception {
+		editor.insertElement(PARA);
+		editor.insertText("before");
+		editor.insertElement(PRE);
+		editor.insertText("within");
+		editor.moveBy(1);
+		editor.insertText("after");
+
+		assertCanMorphOnlyTo(editor /* nothing */);
+	}
+
+	@Test(expected = CannotApplyException.class)
+	public void givenElementWithChildren_whenStructureIsInvalidAfterMorphing_shouldNotMorph() throws Exception {
+		editor.insertElement(PARA);
+		editor.insertText("before");
+		editor.insertElement(PRE);
+		editor.insertText("within");
+		editor.moveBy(1);
+		editor.insertText("after");
+
+		editor.morph(TITLE);
+	}
+
+	@Test
+	public void givenAlternativeElement_whenElementIsNotAllowedAtCurrentInsertionPosition_cannotMorph() throws Exception {
+		editor.insertElement(PARA);
+		editor.moveBy(1);
+		editor.insertElement(PARA);
+
+		assertFalse(editor.canMorph(TITLE));
+	}
+
+	@Test
+	public void givenAlternativeElement_whenElementIsNotAllowedAtCurrentInsertionPosition_shouldNotProvideElementToMorph() throws Exception {
+		editor.insertElement(PARA);
+		editor.moveBy(1);
+		editor.insertElement(PARA);
+
+		assertCanMorphOnlyTo(editor /* nothing */);
+	}
+
+	public void givenElementWithAttributes_whenUndoMorph_shouldPreserveAttributes() throws Exception {
+		editor.insertElement(PARA);
+		editor.setAttribute("id", "idValue");
+		editor.morph(TITLE);
+		editor.undo();
+
+		assertEquals("idValue", editor.getCurrentElement().getAttribute("id").getValue());
+	}
+
+	public void whenReadOnly_cannotUnwrap() throws Exception {
+		editor.insertElement(PARA);
+		editor.insertElement(PRE);
+		editor.insertText("text");
+		editor.setReadOnly(true);
+		assertFalse(editor.canUnwrap());
+	}
+
+	@Test(expected = ReadOnlyException.class)
+	public void whenReadOnly_shouldNotUnwrap() throws Exception {
+		editor.insertElement(PARA);
+		editor.insertElement(PRE);
+		editor.insertText("text");
+		editor.setReadOnly(true);
+		editor.unwrap();
+	}
+
+	@Test
+	public void cannotUnwrapRootElement() throws Exception {
+		assertFalse(editor.canUnwrap());
+	}
+
+	@Test
+	public void unwrapEmptyElement() throws Exception {
+		editor.insertElement(PARA);
+		editor.unwrap();
+
+		editor.selectAll();
+		assertXmlEquals("<section></section>", editor);
+	}
+
+	@Test
+	public void givenInlineElementWithText_shouldUnwrapInlineElement() throws Exception {
+		final IElement para = editor.insertElement(PARA);
+		editor.insertElement(PRE);
+		editor.insertText("text");
+		editor.unwrap();
+
+		assertSame(para, editor.getCurrentElement());
+		editor.selectAll();
+		assertXmlEquals("<section><para>text</para></section>", editor);
+	}
+
+	@Test
+	public void givenElementWithText_whenParentDoesNotAllowText_cannotUnwrap() throws Exception {
+		editor.insertElement(PARA);
+		editor.insertText("text");
+
+		assertFalse(editor.canUnwrap());
+	}
+
+	@Test(expected = CannotApplyException.class)
+	public void givenElementWithText_whenParentDoesNotAllowText_shouldNotUnwrap() throws Exception {
+		editor.insertElement(PARA);
+		editor.insertText("text");
+		editor.unwrap();
+	}
+
+	@Test
+	public void givenElementWithChildren_whenParentDoesNotAllowChildren_cannotUnwrap() throws Exception {
+		editor.insertElement(PARA);
+		editor.insertElement(PRE);
+		editor.moveBy(1);
+
+		assertFalse(editor.canUnwrap());
+	}
+
+	@Test(expected = CannotApplyException.class)
+	public void givenElementWithChildren_whenParentDoesNotAllowChildren_shouldNotUnwrap() throws Exception {
+		editor.insertElement(PARA);
+		editor.insertElement(PRE);
+		editor.moveBy(1);
+		editor.unwrap();
+	}
+
+	@Test
+	public void givenElementWithAttributes_whenUndoUnwrap_shouldPreserveAttributes() throws Exception {
+		editor.insertElement(PARA);
+		editor.insertElement(PRE);
+		editor.setAttribute("id", "idValue");
+
+		editor.unwrap();
+		editor.undo();
+
+		assertEquals(PRE, editor.getCurrentElement().getQualifiedName());
+		assertEquals("idValue", editor.getCurrentElement().getAttributeValue("id"));
+	}
+
+	@Test
+	public void givenMultipleElementsOfSameTypeSelected_canJoin() throws Exception {
+		final IElement firstPara = editor.insertElement(PARA);
+		editor.insertText("1");
+		editor.moveBy(1);
+		editor.insertElement(PARA);
+		editor.insertText("2");
+		editor.moveBy(1);
+		final IElement lastPara = editor.insertElement(PARA);
+		editor.insertText("3");
+		editor.moveBy(1);
+
+		editor.moveTo(firstPara.getStartPosition());
+		editor.moveTo(lastPara.getEndPosition(), true);
+
+		assertTrue(editor.canJoin());
+	}
+
+	@Test
+	public void givenMultipleElementsOfSameTypeSelected_shouldJoin() throws Exception {
+		final IElement firstPara = editor.insertElement(PARA);
+		editor.insertText("1");
+		editor.moveBy(1);
+		editor.insertElement(PARA);
+		editor.insertText("2");
+		editor.moveBy(1);
+		final IElement lastPara = editor.insertElement(PARA);
+		editor.insertText("3");
+		editor.moveBy(1);
+
+		editor.moveTo(firstPara.getStartPosition());
+		editor.moveTo(lastPara.getEndPosition(), true);
+
+		editor.join();
+
+		assertXmlEquals("<section><para>123</para></section>", editor);
+	}
+
+	@Test
+	public void givenMultipleElementsOfSameKindSelected_whenJoining_shouldPreserveAttributesOfFirstElement() throws Exception {
+		final IElement firstPara = editor.insertElement(PARA);
+		editor.insertText("1");
+		editor.moveBy(1);
+		editor.insertElement(PARA);
+		editor.insertText("2");
+		editor.moveBy(1);
+		final IElement lastPara = editor.insertElement(PARA);
+		editor.insertText("3");
+		firstPara.setAttribute("id", "para1");
+		lastPara.setAttribute("id", "para3");
+
+		editor.moveTo(firstPara.getStartPosition());
+		editor.moveTo(lastPara.getEndPosition(), true);
+
+		editor.join();
+
+		assertXmlEquals("<section><para id=\"para1\">123</para></section>", editor);
+	}
+
+	@Test
+	public void givenMultipleElementsOfSameKindSelected_whenJoinUndone_shouldRestoreAttributesOfAllElements() throws Exception {
+		final IElement firstPara = editor.insertElement(PARA);
+		editor.insertText("1");
+		editor.moveBy(1);
+		editor.insertElement(PARA);
+		editor.insertText("2");
+		editor.moveBy(1);
+		final IElement lastPara = editor.insertElement(PARA);
+		editor.insertText("3");
+		firstPara.setAttribute("id", "para1");
+		lastPara.setAttribute("id", "para3");
+
+		editor.moveTo(firstPara.getStartPosition());
+		editor.moveTo(lastPara.getEndPosition(), true);
+
+		editor.join();
+		editor.undo();
+
+		assertXmlEquals("<section><para id=\"para1\">1</para><para>2</para><para id=\"para3\">3</para></section>", editor);
+	}
+
+	@Test
+	public void givenMultipleElementsOfSameKindSelected_whenStructureAfterJoinWouldBeInvalid_cannotJoin() throws Exception {
+		useDocument(createDocumentWithDTD(TEST_DTD, "one-kind-of-child"));
+
+		final IElement firstSection = editor.insertElement(SECTION);
+		editor.insertElement(TITLE);
+		editor.moveBy(2);
+		editor.insertElement(SECTION);
+		editor.insertElement(TITLE);
+		editor.moveBy(2);
+		final IElement lastSection = editor.insertElement(SECTION);
+		editor.insertElement(TITLE);
+
+		editor.moveTo(firstSection.getStartPosition());
+		editor.moveTo(lastSection.getEndPosition(), true);
+
+		assertFalse(editor.canJoin());
+	}
+
+	@Test(expected = CannotApplyException.class)
+	public void givenMultipleElementsOfSameKindSelected_whenStructureAfterJoinWouldBeInvalid_shouldJoin() throws Exception {
+		useDocument(createDocumentWithDTD(TEST_DTD, "one-kind-of-child"));
+
+		final IElement firstSection = editor.insertElement(SECTION);
+		editor.insertElement(TITLE);
+		editor.moveBy(2);
+		editor.insertElement(SECTION);
+		editor.insertElement(TITLE);
+		editor.moveBy(2);
+		final IElement lastSection = editor.insertElement(SECTION);
+		editor.insertElement(TITLE);
+
+		editor.moveTo(firstSection.getStartPosition());
+		editor.moveTo(lastSection.getEndPosition(), true);
+
+		editor.join();
+	}
+
+	@Test
+	public void givenMultipleElementsOfDifferentTypeSelected_cannotJoin() throws Exception {
+		final IElement title = editor.insertElement(TITLE);
+		editor.insertText("1");
+		editor.moveBy(1);
+		editor.insertElement(PARA);
+		editor.insertText("2");
+		editor.moveBy(1);
+		final IElement lastPara = editor.insertElement(PARA);
+		editor.insertText("3");
+		editor.moveBy(1);
+
+		editor.moveTo(title.getStartPosition());
+		editor.moveTo(lastPara.getEndPosition(), true);
+
+		assertFalse(editor.canJoin());
+	}
+
+	@Test(expected = DocumentValidationException.class)
+	public void givenMultipleElementsOfDifferentTypeSelected_shouldNotJoin() throws Exception {
+		final IElement title = editor.insertElement(TITLE);
+		editor.insertText("1");
+		editor.moveBy(1);
+		editor.insertElement(PARA);
+		editor.insertText("2");
+		editor.moveBy(1);
+		final IElement lastPara = editor.insertElement(PARA);
+		editor.insertText("3");
+		editor.moveBy(1);
+
+		editor.moveTo(title.getStartPosition());
+		editor.moveTo(lastPara.getEndPosition(), true);
+
+		editor.join();
+	}
+
+	@Test
+	public void givenSelectionIsEmpty_cannotJoin() throws Exception {
+		assertFalse(editor.canJoin());
+	}
+
+	@Test
+	public void givenSelectionIsEmpty_whenRequestedToJoin_shouldIgnoreGracefully() throws Exception {
+		final String expectedXml = getCurrentXML(editor);
+
+		editor.join();
+
+		assertXmlEquals(expectedXml, editor);
+	}
+
+	@Test
+	public void givenOnlyTextSelected_cannotJoin() throws Exception {
+		final IElement title = editor.insertElement(TITLE);
+		editor.insertText("title text");
+		editor.moveTo(title.getStartPosition().moveBy(1), true);
+
+		assertFalse(editor.canJoin());
+	}
+
+	@Test
+	public void givenOnlyTextSelected_whenRequestedToJoin_shouldIgnoreGracefully() throws Exception {
+		final IElement title = editor.insertElement(TITLE);
+		editor.insertText("title text");
+		editor.selectContentOf(title);
+		final String expectedXml = getCurrentXML(editor);
+
+		editor.join();
+
+		assertXmlEquals(expectedXml, editor);
+	}
+
+	@Test
+	public void givenOnlySingleElementSelected_cannotJoin() throws Exception {
+		final IElement title = editor.insertElement(TITLE);
+		editor.moveTo(title.getStartPosition(), true);
+
+		assertFalse(editor.canJoin());
+	}
+
+	@Test
+	public void givenOnlySingleElementSelected_whenRequestedToJoin_shouldIgnoreGracefully() throws Exception {
+		final IElement title = editor.insertElement(TITLE);
+		editor.moveTo(title.getStartPosition(), true);
+		final String expectedXml = getCurrentXML(editor);
+
+		editor.join();
+
+		assertXmlEquals(expectedXml, editor);
+	}
+
+	@Test
+	public void givenMultipleCommentsSelected_canJoin() throws Exception {
+		final IComment firstComment = editor.insertComment();
+		editor.insertText("comment1");
+		editor.moveBy(1);
+		editor.insertComment();
+		editor.insertText("comment2");
+		editor.moveBy(1);
+		final IComment lastComment = editor.insertComment();
+		editor.insertText("comment3");
+
+		editor.moveTo(firstComment.getStartPosition());
+		editor.moveTo(lastComment.getEndPosition(), true);
+
+		assertTrue(editor.canJoin());
+	}
+
+	@Test
+	public void givenMultipleCommentsSelected_shouldJoin() throws Exception {
+		final IComment firstComment = editor.insertComment();
+		editor.insertText("comment1");
+		editor.moveBy(1);
+		editor.insertComment();
+		editor.insertText("comment2");
+		editor.moveBy(1);
+		final IComment lastComment = editor.insertComment();
+		editor.insertText("comment3");
+
+		editor.moveTo(firstComment.getStartPosition());
+		editor.moveTo(lastComment.getEndPosition(), true);
+
+		editor.join();
+
+		assertXmlEquals("<section><!--comment1comment2comment3--></section>", editor);
+	}
+
+	@Test
+	public void givenMultipleInlineElementsOfSameKindSelected_whenTextEndsWithSpace_shouldJoin() throws Exception {
+		editor.insertElement(PARA);
+		final IElement firstElement = editor.insertElement(PRE);
+		editor.insertText("1");
+		editor.moveBy(1);
+		final IElement lastElement = editor.insertElement(PRE);
+		editor.insertText("2 ");
+
+		editor.moveTo(firstElement.getStartPosition());
+		editor.moveTo(lastElement.getEndPosition(), true);
+
+		editor.join();
+
+		assertXmlEquals("<section><para><pre>12 </pre></para></section>", editor);
+	}
+
+	@Test
+	public void givenMultipleInlineElementsOfSameKindSelected_whenTextBetweenElements_cannotJoin() throws Exception {
+		editor.insertElement(PARA);
+		final IElement firstElement = editor.insertElement(PRE);
+		editor.insertText("1");
+		editor.moveBy(1);
+		editor.insertText("text between elements");
+		final IElement lastElement = editor.insertElement(PRE);
+		editor.insertText("2 ");
+
+		editor.moveTo(firstElement.getStartPosition());
+		editor.moveTo(lastElement.getEndPosition(), true);
+
+		assertFalse(editor.canJoin());
+	}
+
+	@Test(expected = DocumentValidationException.class)
+	public void givenMultipleInlineElementsOfSameKindSelected_whenTextBetweenElements_shouldNotJoin() throws Exception {
+		editor.insertElement(PARA);
+		final IElement firstElement = editor.insertElement(PRE);
+		editor.insertText("1");
+		editor.moveBy(1);
+		editor.insertText("text between elements");
+		final IElement lastElement = editor.insertElement(PRE);
+		editor.insertText("2 ");
+
+		editor.moveTo(firstElement.getStartPosition());
+		editor.moveTo(lastElement.getEndPosition(), true);
+
+		editor.join();
+	}
+
+	@Test
+	public void givenDeletedText_whenDeleteUndone_shouldSetCaretToEndOfRecoveredText() throws Exception {
+		final IElement title = editor.insertElement(TITLE);
+		editor.insertText("Hello World");
+		editor.moveTo(title.getStartPosition().moveBy(1));
+		editor.moveBy(5, true);
+		final int expectedCaretPosition = editor.getSelectedRange().getEndOffset();
+
+		editor.deleteSelection();
+		editor.undo();
+
+		assertEquals(expectedCaretPosition, editor.getCaretPosition().getOffset());
+	}
+
+	@Test
+	public void afterDeletingSelection_CaretPositionShouldBeValid() throws Exception {
+		final IElement para = editor.insertElement(PARA);
+		editor.moveTo(para.getEndPosition());
+		final IElement pre = editor.insertElement(PRE);
+		editor.insertText("Hello World");
+		editor.moveTo(pre.getStartPosition());
+		editor.moveTo(pre.getEndPosition().moveBy(-1), true);
+
+		editor.deleteSelection();
+		assertEquals(para.getEndPosition(), editor.getCaretPosition());
+	}
+
+	@Test
+	public void undoAndRedoDelete() throws Exception {
+		final IElement para = editor.insertElement(PARA);
+		editor.moveTo(para.getEndPosition());
+		final IElement pre = editor.insertElement(PRE);
+		editor.insertText("Hello World");
+		final String beforeDeleteXml = getCurrentXML(editor);
+
+		editor.moveTo(pre.getStartPosition());
+		editor.moveTo(pre.getEndPosition().moveBy(-1), true);
+		editor.deleteSelection();
+
+		final String beforeUndoXml = getCurrentXML(editor);
+		editor.undo();
+		assertXmlEquals(beforeDeleteXml, editor);
+
+		editor.redo();
+		assertXmlEquals(beforeUndoXml, editor);
+	}
+
+	private static StyleSheet readTestStyleSheet() throws IOException {
+		return new StyleSheetReader().read(TestResources.get("test.css"));
+	}
+
+}
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/L2XIncludeEditingTest.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/L2XIncludeEditingTest.java
index 5a1f156..2298a7d 100644
--- a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/L2XIncludeEditingTest.java
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/L2XIncludeEditingTest.java
@@ -1,132 +1,128 @@
-/*******************************************************************************

- * Copyright (c) 2014 Carsten Hiesserich 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:

- * 		Carsten Hiesserich - initial API and implementation

- *******************************************************************************/

-package org.eclipse.vex.core.internal.widget;

-

-import static org.eclipse.vex.core.internal.widget.VexWidgetTest.createDocumentWithDTD;

-import static org.eclipse.vex.core.internal.widget.VexWidgetTest.getCurrentXML;

-import static org.eclipse.vex.core.tests.TestResources.TEST_DTD;

-import static org.junit.Assert.assertEquals;

-import static org.junit.Assert.assertFalse;

-import static org.junit.Assert.assertNull;

-import static org.junit.Assert.assertTrue;

-

-import org.eclipse.vex.core.internal.css.CssWhitespacePolicy;

-import org.eclipse.vex.core.internal.css.StyleSheet;

-import org.eclipse.vex.core.internal.io.XMLFragment;

-import org.eclipse.vex.core.provisional.dom.IElement;

-import org.eclipse.vex.core.provisional.dom.IIncludeNode;

-import org.junit.Before;

-import org.junit.Test;

-

-/**

- * @author Florian Thienel

- */

-public class L2XIncludeEditingTest {

-

-	private final String includeXml = "<para>12<xi:include xmlns:xi=\"http://www.w3.org/2001/XInclude\" href=\"test\" />34</para>";

-	private final String includeInlineXml = "<para>12<xi:include xmlns:xi=\"http://www.w3.org/2001/XInclude\" href=\"test\" parse=\"text\" />34</para>";

-

-	private IVexWidget widget;

-	private IElement rootElement;

-	private IElement para;

-	private IIncludeNode includeNode;

-

-	@Before

-	public void setUp() throws Exception {

-		widget = new BaseVexWidget(new MockHostComponent());

-		widget.setDocument(createDocumentWithDTD(TEST_DTD, "section"), StyleSheet.NULL);

-		widget.setWhitespacePolicy(new CssWhitespacePolicy(widget.getStyleSheet()));

-	}

-

-	@Test

-	public void whenCaretAfterEmptyInclude_BackspaceShouldDeleteInclude() throws Exception {

-		createIncludeElement(includeXml);

-		widget.moveTo(includeNode.getEndPosition().moveBy(1));

-		widget.deletePreviousChar();

-

-		assertTrue(para.children().withoutText().isEmpty());

-		assertFalse(includeNode.isAssociated());

-		assertNull(includeNode.getReference().getParent());

-	}

-

-	@Test

-	public void whenIncludeSelected_ShouldDeleteInclude() throws Exception {

-		createIncludeElement(includeXml);

-		widget.moveTo(includeNode.getStartPosition());

-		widget.moveTo(includeNode.getEndPosition(), true);

-		widget.deleteSelection();

-

-		final String currentXml = getCurrentXML(widget);

-		assertEquals("<section><para>1234</para></section>", currentXml);

-

-		assertTrue(para.children().withoutText().isEmpty());

-		assertFalse(includeNode.isAssociated());

-		assertNull(includeNode.getReference().getParent());

-	}

-

-	@Test

-	public void deleteInlineIncludeWithSurroundingContent() throws Exception {

-		createIncludeElement(includeInlineXml);

-

-		widget.moveTo(includeNode.getStartPosition());

-		widget.moveTo(para.getEndPosition().moveBy(-1), true);

-		widget.deleteSelection();

-

-		final String currentXml = getCurrentXML(widget);

-		assertEquals("<section><para>124</para></section>", currentXml);

-

-		assertTrue(para.children().withoutText().isEmpty());

-		assertFalse(includeNode.isAssociated());

-		assertNull(includeNode.getReference().getParent());

-	}

-

-	@Test

-	public void deleteInlineIncludeWithSurroundingContent_selectingBackwards() throws Exception {

-		createIncludeElement(includeInlineXml);

-

-		// Yes, the direction of the selection makes a difference

-		widget.moveTo(para.getEndPosition().moveBy(-1));

-		widget.moveTo(includeNode.getStartPosition(), true);

-

-		widget.deleteSelection();

-

-		final String currentXml = getCurrentXML(widget);

-		assertEquals("<section><para>124</para></section>", currentXml);

-

-		assertTrue(para.children().withoutText().isEmpty());

-		assertFalse(includeNode.isAssociated());

-		assertNull(includeNode.getReference().getParent());

-	}

-

-	@Test

-	public void undoDeleteInclude() throws Exception {

-		createIncludeElement(includeXml);

-		widget.moveTo(includeNode.getStartPosition());

-		widget.moveTo(includeNode.getEndPosition(), true);

-		final String exepctedXml = getCurrentXML(widget);

-		widget.deleteSelection();

-		widget.undo();

-		assertEquals(exepctedXml, getCurrentXML(widget));

-

-		para = (IElement) rootElement.children().get(0);

-		includeNode = (IIncludeNode) para.children().withoutText().get(0);

-		assertTrue(includeNode.isAssociated());

-		assertEquals(para, includeNode.getReference().getParent());

-	}

-

-	private void createIncludeElement(final String xml) {

-		widget.insertFragment(new XMLFragment(xml).getDocumentFragment());

-		rootElement = widget.getDocument().getRootElement();

-		para = (IElement) rootElement.children().get(0);

-		includeNode = (IIncludeNode) para.children().withoutText().get(0);

-	}

-

-}

+/*******************************************************************************
+ * Copyright (c) 2014 Carsten Hiesserich 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:
+ * 		Carsten Hiesserich - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.vex.core.internal.widget;
+
+import static org.eclipse.vex.core.internal.widget.VexWidgetTest.createDocumentWithDTD;
+import static org.eclipse.vex.core.internal.widget.VexWidgetTest.getCurrentXML;
+import static org.eclipse.vex.core.tests.TestResources.TEST_DTD;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import org.eclipse.vex.core.internal.io.XMLFragment;
+import org.eclipse.vex.core.provisional.dom.IDocument;
+import org.eclipse.vex.core.provisional.dom.IElement;
+import org.eclipse.vex.core.provisional.dom.IIncludeNode;
+import org.junit.Before;
+import org.junit.Test;
+
+public class L2XIncludeEditingTest {
+
+	private final String includeXml = "<para>12<xi:include xmlns:xi=\"http://www.w3.org/2001/XInclude\" href=\"test\" />34</para>";
+	private final String includeInlineXml = "<para>12<xi:include xmlns:xi=\"http://www.w3.org/2001/XInclude\" href=\"test\" parse=\"text\" />34</para>";
+
+	private IDocumentEditor editor;
+	private IElement rootElement;
+	private IElement para;
+	private IIncludeNode includeNode;
+
+	@Before
+	public void setUp() throws Exception {
+		final IDocument document = createDocumentWithDTD(TEST_DTD, "section");
+		editor = new DocumentEditor(new FakeCursor(document));
+		editor.setDocument(document);
+	}
+
+	@Test
+	public void whenCaretAfterEmptyInclude_BackspaceShouldDeleteInclude() throws Exception {
+		createIncludeElement(includeXml);
+		editor.moveTo(includeNode.getEndPosition().moveBy(1));
+		editor.deleteBackward();
+
+		assertTrue(para.children().withoutText().isEmpty());
+		assertFalse(includeNode.isAssociated());
+		assertNull(includeNode.getReference().getParent());
+	}
+
+	@Test
+	public void whenIncludeSelected_ShouldDeleteInclude() throws Exception {
+		createIncludeElement(includeXml);
+		editor.moveTo(includeNode.getStartPosition());
+		editor.moveTo(includeNode.getEndPosition(), true);
+		editor.deleteSelection();
+
+		final String currentXml = getCurrentXML(editor);
+		assertEquals("<section><para>1234</para></section>", currentXml);
+
+		assertTrue(para.children().withoutText().isEmpty());
+		assertFalse(includeNode.isAssociated());
+		assertNull(includeNode.getReference().getParent());
+	}
+
+	@Test
+	public void deleteInlineIncludeWithSurroundingContent() throws Exception {
+		createIncludeElement(includeInlineXml);
+
+		editor.moveTo(includeNode.getStartPosition());
+		editor.moveTo(para.getEndPosition().moveBy(-1), true);
+		editor.deleteSelection();
+
+		final String currentXml = getCurrentXML(editor);
+		assertEquals("<section><para>124</para></section>", currentXml);
+
+		assertTrue(para.children().withoutText().isEmpty());
+		assertFalse(includeNode.isAssociated());
+		assertNull(includeNode.getReference().getParent());
+	}
+
+	@Test
+	public void deleteInlineIncludeWithSurroundingContent_selectingBackwards() throws Exception {
+		createIncludeElement(includeInlineXml);
+
+		// Yes, the direction of the selection makes a difference
+		editor.moveTo(para.getEndPosition().moveBy(-1));
+		editor.moveTo(includeNode.getStartPosition(), true);
+
+		editor.deleteSelection();
+
+		final String currentXml = getCurrentXML(editor);
+		assertEquals("<section><para>124</para></section>", currentXml);
+
+		assertTrue(para.children().withoutText().isEmpty());
+		assertFalse(includeNode.isAssociated());
+		assertNull(includeNode.getReference().getParent());
+	}
+
+	@Test
+	public void undoDeleteInclude() throws Exception {
+		createIncludeElement(includeXml);
+		editor.moveTo(includeNode.getStartPosition());
+		editor.moveTo(includeNode.getEndPosition(), true);
+		final String exepctedXml = getCurrentXML(editor);
+		editor.deleteSelection();
+		editor.undo();
+		assertEquals(exepctedXml, getCurrentXML(editor));
+
+		para = (IElement) rootElement.children().get(0);
+		includeNode = (IIncludeNode) para.children().withoutText().get(0);
+		assertTrue(includeNode.isAssociated());
+		assertEquals(para, includeNode.getReference().getParent());
+	}
+
+	private void createIncludeElement(final String xml) {
+		editor.insertFragment(new XMLFragment(xml).getDocumentFragment());
+		rootElement = editor.getDocument().getRootElement();
+		para = (IElement) rootElement.children().get(0);
+		includeNode = (IIncludeNode) para.children().withoutText().get(0);
+	}
+
+}
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/L2XmlInsertionTest.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/L2XmlInsertionTest.java
index cde5166..3af70f5 100644
--- a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/L2XmlInsertionTest.java
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/L2XmlInsertionTest.java
@@ -28,10 +28,11 @@
 import org.eclipse.vex.core.internal.dom.GapContent;
 import org.eclipse.vex.core.internal.dom.Node;
 import org.eclipse.vex.core.internal.io.XMLFragment;
-import org.eclipse.vex.core.internal.undo.CannotRedoException;
+import org.eclipse.vex.core.internal.undo.CannotApplyException;
 import org.eclipse.vex.core.provisional.dom.ContentRange;
 import org.eclipse.vex.core.provisional.dom.DocumentValidationException;
 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;
 import org.eclipse.vex.core.provisional.dom.INode;
@@ -46,42 +47,43 @@
 	private static final QualifiedName PRE = new QualifiedName(null, "pre");
 	private static final QualifiedName EMPHASIS = new QualifiedName(null, "emphasis");
 
-	private BaseVexWidget widget;
+	private IDocumentEditor editor;
 	private IElement para1;
 	private IElement pre;
 
 	@Before
 	public void setUp() throws Exception {
-		widget = new BaseVexWidget(new MockHostComponent());
 		final StyleSheet styleSheet = new StyleSheetReader().read(TestResources.get("test.css"));
-		widget.setDocument(createDocumentWithDTD(TEST_DTD, "section"), styleSheet);
-		widget.setWhitespacePolicy(new CssWhitespacePolicy(styleSheet));
-		para1 = widget.insertElement(PARA);
-		widget.moveBy(1);
-		widget.insertElement(PARA);
-		widget.moveTo(para1.getEndPosition());
-		pre = widget.insertElement(PRE);
+		final IDocument document = createDocumentWithDTD(TEST_DTD, "section");
+		editor = new DocumentEditor(new FakeCursor(document), new CssWhitespacePolicy(styleSheet));
+		editor.setDocument(document);
+
+		para1 = editor.insertElement(PARA);
+		editor.moveBy(1);
+		editor.insertElement(PARA);
+		editor.moveTo(para1.getEndPosition());
+		pre = editor.insertElement(PRE);
 	}
 
 	@Test
 	public void givenNonPreElement_whenInsertingNotAllowedXML_shouldInsertTextOnly() throws Exception {
-		widget.moveTo(para1.getStartPosition().moveBy(1));
-		widget.insertXML(createParaXML());
+		editor.moveTo(para1.getStartPosition().moveBy(1));
+		editor.insertXML(createParaXML());
 
 		assertEquals("beforeinnerafter", para1.getText());
 	}
 
 	@Test(expected = DocumentValidationException.class)
 	public void givenNonPreElement_whenInsertingInvalidXML_shouldThrowDocumentValidationExeption() throws Exception {
-		widget.moveTo(para1.getStartPosition().moveBy(1));
+		editor.moveTo(para1.getStartPosition().moveBy(1));
 
-		widget.insertXML("<emphasis>someText</para>");
+		editor.insertXML("<emphasis>someText</para>");
 	}
 
 	@Test
 	public void givenNonPreElement_whenInsertingValidXML_shouldInsertXml() throws Exception {
-		widget.moveTo(para1.getEndPosition());
-		widget.insertXML(createInlineXML("before", "inner", "after"));
+		editor.moveTo(para1.getEndPosition());
+		editor.insertXML(createInlineXML("before", "inner", "after"));
 
 		final List<? extends INode> children = para1.children().asList();
 		assertTrue("Expecting IParent", children.get(0) instanceof IParent); // the pre element
@@ -95,16 +97,16 @@
 
 	@Test
 	public void givenPreElement_whenInsertingInvalidXML_shouldInsertTextWithWhitespace() throws Exception {
-		widget.moveTo(pre.getStartPosition().moveBy(1));
-		widget.insertXML(createParaXML());
+		editor.moveTo(pre.getStartPosition().moveBy(1));
+		editor.insertXML(createParaXML());
 
 		assertEquals("beforeinnerafter", pre.getText());
 	}
 
 	@Test
 	public void givenPreElement_whenInsertingValidXML_shouldInsertXML() throws Exception {
-		widget.moveTo(pre.getEndPosition());
-		widget.insertXML(createInlineXML("before", "inner", "after"));
+		editor.moveTo(pre.getEndPosition());
+		editor.insertXML(createInlineXML("before", "inner", "after"));
 
 		final List<? extends INode> children = pre.children().asList();
 		assertTrue("Expecting IText", children.get(0) instanceof IText);
@@ -117,8 +119,8 @@
 
 	@Test
 	public void givenPreElement_whenInsertingValidXMLWithWhitespace_shouldKeepWhitespace() throws Exception {
-		widget.moveTo(pre.getEndPosition());
-		widget.insertXML(createInlineXML("line1\nline2   end", "inner", "after"));
+		editor.moveTo(pre.getEndPosition());
+		editor.insertXML(createInlineXML("line1\nline2   end", "inner", "after"));
 
 		final List<? extends INode> children = pre.children().asList();
 		assertTrue("Expecting IText", children.get(0) instanceof IText);
@@ -131,8 +133,8 @@
 
 	@Test
 	public void whenInsertingMixedXMLWithWhitespace_shouldKeepWhitecpaceInPre() throws Exception {
-		widget.moveTo(para1.getEndPosition());
-		widget.insertXML("before<pre>pre1\npre2  end</pre>after   \nend");
+		editor.moveTo(para1.getEndPosition());
+		editor.insertXML("before<pre>pre1\npre2  end</pre>after   \nend");
 
 		final List<? extends INode> children = para1.children().after(pre.getEndPosition().getOffset()).asList();
 		assertEquals("New children count", 3, children.size());
@@ -144,16 +146,16 @@
 		assertEquals("after end", children.get(2).getText());
 	}
 
-	@Test(expected = CannotRedoException.class)
+	@Test(expected = CannotApplyException.class)
 	public void givenNonPreElement_whenInsertingNotAllowedFragment_shouldThrowCannotRedoException() throws Exception {
-		widget.moveTo(para1.getStartPosition().moveBy(1));
-		widget.insertFragment(createParaFragment());
+		editor.moveTo(para1.getStartPosition().moveBy(1));
+		editor.insertFragment(createParaFragment());
 	}
 
 	@Test
 	public void givenNonPreElement_whenInsertingValidFragment_shouldInsertXml() throws Exception {
-		widget.moveTo(para1.getEndPosition());
-		widget.insertFragment(createInlineFragment("before", "inner", "after"));
+		editor.moveTo(para1.getEndPosition());
+		editor.insertFragment(createInlineFragment("before", "inner", "after"));
 
 		final List<? extends INode> children = para1.children().asList();
 		assertTrue("Expecting IParent", children.get(0) instanceof IParent); // the pre element
@@ -165,16 +167,16 @@
 		assertEquals("after", children.get(3).getText());
 	}
 
-	@Test(expected = CannotRedoException.class)
+	@Test(expected = CannotApplyException.class)
 	public void givenPreElement_whenInsertingInvalidFragment_shouldThrowCannotRedoException() throws Exception {
-		widget.moveTo(pre.getStartPosition().moveBy(1));
-		widget.insertFragment(createParaFragment());
+		editor.moveTo(pre.getStartPosition().moveBy(1));
+		editor.insertFragment(createParaFragment());
 	}
 
 	@Test
 	public void givenPreElement_whenInsertingValidFragment_shouldInsertXML() throws Exception {
-		widget.moveTo(pre.getEndPosition());
-		widget.insertFragment(createInlineFragment("before", "inner", "after"));
+		editor.moveTo(pre.getEndPosition());
+		editor.insertFragment(createInlineFragment("before", "inner", "after"));
 
 		final List<? extends INode> children = pre.children().asList();
 		assertTrue("Expecting IText", children.get(0) instanceof IText);
@@ -187,8 +189,8 @@
 
 	@Test
 	public void givenPreElement_whenInsertingValidFragmentWithWhitespace_shouldKeepWhitespace() throws Exception {
-		widget.moveTo(pre.getEndPosition());
-		widget.insertFragment(createInlineFragment("line1\nline2   end", "inner", "after"));
+		editor.moveTo(pre.getEndPosition());
+		editor.insertFragment(createInlineFragment("line1\nline2   end", "inner", "after"));
 
 		final List<? extends INode> children = pre.children().asList();
 		assertTrue("Expecting IText", children.get(0) instanceof IText);
@@ -201,8 +203,8 @@
 
 	@Test
 	public void whenInsertingMixedFragmentWithWhitespace_shouldKeepWhitecpaceInPre() throws Exception {
-		widget.moveTo(para1.getEndPosition());
-		widget.insertFragment(new XMLFragment("before<pre>pre1\npre2  end</pre>after   \nend").getDocumentFragment());
+		editor.moveTo(para1.getEndPosition());
+		editor.insertFragment(new XMLFragment("before<pre>pre1\npre2  end</pre>after   \nend").getDocumentFragment());
 
 		final List<? extends INode> children = para1.children().after(pre.getEndPosition().getOffset()).asList();
 		assertEquals("New children count", 3, children.size());
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/VexWidgetTest.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/VexWidgetTest.java
index 8159954..58e541e 100644
--- a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/VexWidgetTest.java
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/VexWidgetTest.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2010, Florian Thienel and others.
+ * Copyright (c) 2010, 2016 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
@@ -18,7 +18,6 @@
 import java.util.Arrays;
 
 import org.eclipse.core.runtime.QualifiedName;
-import org.eclipse.vex.core.internal.css.StyleSheet;
 import org.eclipse.vex.core.internal.dom.Document;
 import org.eclipse.vex.core.internal.io.XMLFragment;
 import org.eclipse.vex.core.internal.validator.WTPVEXValidator;
@@ -39,101 +38,106 @@
 	public static final QualifiedName PARA = new QualifiedName(null, "para");
 	public static final QualifiedName PRE = new QualifiedName(null, "pre");
 
-	private IVexWidget widget;
+	private IDocumentEditor editor;
+	private FakeCursor cursor;
 
 	@Before
 	public void setUp() throws Exception {
-		widget = new BaseVexWidget(new MockHostComponent());
+		cursor = new FakeCursor(null);
+		editor = new DocumentEditor(cursor);
+	}
+
+	private void useDocument(final IDocument document) {
+		cursor.setDocument(document);
+		editor.setDocument(document);
 	}
 
 	@Test
 	public void provideOnlyAllowedElementsFromDtd() throws Exception {
-		widget.setDocument(createDocumentWithDTD(TEST_DTD, "section"), StyleSheet.NULL);
-		assertCanInsertOnly(widget, "title", "para");
-		widget.insertElement(new QualifiedName(null, "title"));
-		assertCanInsertOnly(widget);
-		widget.moveBy(1);
-		assertCanInsertOnly(widget, "para");
-		widget.insertElement(new QualifiedName(null, "para"));
-		widget.moveBy(1);
-		assertCanInsertOnly(widget, "para");
+		useDocument(createDocumentWithDTD(TEST_DTD, "section"));
+		assertCanInsertOnly(editor, "title", "para");
+		editor.insertElement(new QualifiedName(null, "title"));
+		assertCanInsertOnly(editor);
+		editor.moveBy(1);
+		assertCanInsertOnly(editor, "para");
+		editor.insertElement(new QualifiedName(null, "para"));
+		editor.moveBy(1);
+		assertCanInsertOnly(editor, "para");
 	}
 
 	@Test
 	public void provideOnlyAllowedElementsFromSimpleSchema() throws Exception {
-		widget.setDocument(createDocument(CONTENT_NS, "p"), StyleSheet.NULL);
-		assertCanInsertOnly(widget, "b", "i");
-		widget.insertElement(new QualifiedName(CONTENT_NS, "b"));
-		assertCanInsertOnly(widget, "b", "i");
-		widget.moveBy(1);
-		assertCanInsertOnly(widget, "b", "i");
+		useDocument(createDocument(CONTENT_NS, "p"));
+		assertCanInsertOnly(editor, "b", "i");
+		editor.insertElement(new QualifiedName(CONTENT_NS, "b"));
+		assertCanInsertOnly(editor, "b", "i");
+		editor.moveBy(1);
+		assertCanInsertOnly(editor, "b", "i");
 	}
 
 	@Test
 	public void provideOnlyAllowedElementFromComplexSchema() throws Exception {
-		widget.setDocument(createDocument(STRUCTURE_NS, "chapter"), StyleSheet.NULL);
-		assertCanInsertOnly(widget, "title", "chapter", "p");
-		widget.insertElement(new QualifiedName(STRUCTURE_NS, "title"));
-		assertCanInsertOnly(widget);
-		widget.moveBy(1);
+		useDocument(createDocument(STRUCTURE_NS, "chapter"));
+		assertCanInsertOnly(editor, "title", "chapter", "p");
+		editor.insertElement(new QualifiedName(STRUCTURE_NS, "title"));
+		assertCanInsertOnly(editor);
+		editor.moveBy(1);
 		//		assertCanInsertOnly(widget, "chapter", "p");
-		widget.insertElement(new QualifiedName(CONTENT_NS, "p"));
-		assertCanInsertOnly(widget, "b", "i");
-		widget.moveBy(1);
+		editor.insertElement(new QualifiedName(CONTENT_NS, "p"));
+		assertCanInsertOnly(editor, "b", "i");
+		editor.moveBy(1);
 		//		assertCanInsertOnly(widget, "p");
 		// FIXME: maybe the schema is still not what I mean
 	}
 
 	@Test
 	public void provideNoAllowedElementsForInsertionInComment() throws Exception {
-		final BaseVexWidget widget = new BaseVexWidget(new MockHostComponent());
-		final IDocument document = createDocument(STRUCTURE_NS, "chapter");
-		widget.setDocument(document, StyleSheet.NULL);
-		widget.insertElement(new QualifiedName(STRUCTURE_NS, "title"));
-		widget.moveBy(1);
-		widget.insertElement(new QualifiedName(CONTENT_NS, "p"));
-		widget.insertComment();
+		useDocument(createDocument(STRUCTURE_NS, "chapter"));
+		editor.insertElement(new QualifiedName(STRUCTURE_NS, "title"));
+		editor.moveBy(1);
+		editor.insertElement(new QualifiedName(CONTENT_NS, "p"));
+		editor.insertComment();
 
-		assertCannotInsertAnything(widget);
+		assertCannotInsertAnything(editor);
 	}
 
 	@Test
 	public void undoRemoveCommentTag() throws Exception {
-		final BaseVexWidget widget = new BaseVexWidget(new MockHostComponent());
-		widget.setDocument(createDocument(STRUCTURE_NS, "chapter"), StyleSheet.NULL);
-		widget.insertElement(new QualifiedName(CONTENT_NS, "p"));
-		widget.insertText("1text before comment1");
-		final INode comment = widget.insertComment();
-		widget.insertText("2comment text2");
-		widget.moveBy(1);
-		widget.insertText("3text after comment3");
+		useDocument(createDocument(STRUCTURE_NS, "chapter"));
+		editor.insertElement(new QualifiedName(CONTENT_NS, "p"));
+		editor.insertText("1text before comment1");
+		final INode comment = editor.insertComment();
+		editor.insertText("2comment text2");
+		editor.moveBy(1);
+		editor.insertText("3text after comment3");
 
-		final String expectedContentStructure = getContentStructure(widget.getDocument().getRootElement());
+		final String expectedContentStructure = getContentStructure(editor.getDocument().getRootElement());
 
-		widget.doWork(new Runnable() {
+		editor.doWork(new Runnable() {
 			@Override
 			public void run() {
-				widget.selectContentOf(comment);
-				final IDocumentFragment fragment = widget.getSelectedFragment();
-				widget.deleteSelection();
+				editor.selectContentOf(comment);
+				final IDocumentFragment fragment = editor.getSelectedFragment();
+				editor.deleteSelection();
 
-				widget.select(comment);
-				widget.deleteSelection();
+				editor.select(comment);
+				editor.deleteSelection();
 
-				widget.insertFragment(fragment);
+				editor.insertFragment(fragment);
 			}
 		});
 
-		widget.undo();
+		editor.undo();
 
-		assertEquals(expectedContentStructure, getContentStructure(widget.getDocument().getRootElement()));
+		assertEquals(expectedContentStructure, getContentStructure(editor.getDocument().getRootElement()));
 	}
 
 	/* bug 421401 */
 	@Test
 	public void whenClickedRightToLineEnd_shouldSetCursorToLineEnd() throws Exception {
-		final IDocument document = createDocument(STRUCTURE_NS, "chapter");
-		widget.setDocument(document, StyleSheet.NULL);
+		// TODO move to a separate test class that is specific to BaseVexWidget, this here is not editing but mouse handling
+		final BaseVexWidget widget = new BaseVexWidget(new MockHostComponent());
+		widget.setDocument(createDocument(STRUCTURE_NS, "chapter"));
 		widget.insertElement(new QualifiedName(CONTENT_NS, "p"));
 		widget.insertText("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris.");
 		widget.setLayoutWidth(200); // breaks after "amet, "
@@ -145,18 +149,6 @@
 		assertEquals("first position", 31, firstPositionInSecondLine.getOffset());
 	}
 
-	@Test
-	public void whenMovingCursorUp_shouldNotGetStuckInHigherLevelElement() throws Exception {
-		final IDocument document = createDocument(STRUCTURE_NS, "chapter");
-		widget.setDocument(document, StyleSheet.NULL);
-		widget.insertElement(new QualifiedName(CONTENT_NS, "p"));
-		widget.insertText("para 1");
-		widget.moveTo(new ContentPosition(document, 1));
-
-		widget.moveToNextLine(false);
-		assertEquals(2, widget.getCaretPosition().getOffset());
-	}
-
 	public static IDocument createDocumentWithDTD(final String dtdIdentifier, final String rootElementName) {
 		final IValidator validator = new WTPVEXValidator(dtdIdentifier);
 		final Document document = new Document(new QualifiedName(null, rootElementName));
@@ -171,19 +163,19 @@
 		return document;
 	}
 
-	public static void assertCanInsertOnly(final IVexWidget widget, final Object... elementNames) {
+	public static void assertCanInsertOnly(final IDocumentEditor widget, final Object... elementNames) {
 		final String[] expected = sortedCopyOf(elementNames);
 		final String[] actual = sortedCopyOf(widget.getValidInsertElements());
 		assertEquals(Arrays.toString(expected), Arrays.toString(actual));
 	}
 
-	public static void assertCanMorphOnlyTo(final IVexWidget widget, final Object... elementNames) {
+	public static void assertCanMorphOnlyTo(final IDocumentEditor widget, final Object... elementNames) {
 		final String[] expected = sortedCopyOf(elementNames);
 		final String[] actual = sortedCopyOf(widget.getValidMorphElements());
 		assertEquals(Arrays.toString(expected), Arrays.toString(actual));
 	}
 
-	public static void assertCannotInsertAnything(final IVexWidget widget) {
+	public static void assertCannotInsertAnything(final IDocumentEditor widget) {
 		assertCanInsertOnly(widget /* nothing */);
 	}
 
@@ -221,11 +213,11 @@
 		return result.toString();
 	}
 
-	public static String getCurrentXML(final IVexWidget widget) {
+	public static String getCurrentXML(final IDocumentEditor widget) {
 		return new XMLFragment(widget.getDocument().getFragment(widget.getDocument().getRootElement().getRange())).getXML();
 	}
 
-	public static void assertXmlEquals(final String expected, final IVexWidget widget) {
+	public static void assertXmlEquals(final String expected, final IDocumentEditor widget) {
 		assertEquals(expected, getCurrentXML(widget));
 	}
 }
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/swt/SwtClipboardTest.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/swt/SwtClipboardTest.java
new file mode 100644
index 0000000..548a948
--- /dev/null
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/swt/SwtClipboardTest.java
@@ -0,0 +1,24 @@
+/*******************************************************************************
+ * Copyright (c) 2016 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.widget.swt;
+
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.vex.core.internal.widget.BaseClipboardTest;
+import org.eclipse.vex.core.internal.widget.IClipboard;
+
+public class SwtClipboardTest extends BaseClipboardTest {
+
+	@Override
+	protected IClipboard createClipboard() {
+		return new SwtClipboard(Display.getDefault());
+	}
+
+}
diff --git a/org.eclipse.vex.core/META-INF/MANIFEST.MF b/org.eclipse.vex.core/META-INF/MANIFEST.MF
index b15d186..8ae9532 100644
--- a/org.eclipse.vex.core/META-INF/MANIFEST.MF
+++ b/org.eclipse.vex.core/META-INF/MANIFEST.MF
@@ -17,13 +17,16 @@
  org.eclipse.jface.text;bundle-version="[3.8.0,4.0.0)"
 Export-Package: org.eclipse.vex.core,
  org.eclipse.vex.core.internal;x-friends:="org.eclipse.vex.ui,org.eclipse.vex.core.tests,org.eclipse.vex.ui.tests",
+ org.eclipse.vex.core.internal.boxes;x-friends:="org.eclipse.vex.core.tests,org.eclipse.vex.ui",
  org.eclipse.vex.core.internal.core;x-friends:="org.eclipse.vex.ui,org.eclipse.vex.core.tests,org.eclipse.vex.ui.tests",
  org.eclipse.vex.core.internal.css;x-friends:="org.eclipse.vex.ui,org.eclipse.vex.core.tests,org.eclipse.vex.ui.tests",
+ org.eclipse.vex.core.internal.cursor;x-friends:="org.eclipse.vex.core.tests,org.eclipse.vex.ui",
  org.eclipse.vex.core.internal.dom;x-friends:="org.eclipse.vex.ui,org.eclipse.vex.core.tests,org.eclipse.vex.ui.tests",
  org.eclipse.vex.core.internal.io;x-friends:="org.eclipse.vex.core.tests,org.eclipse.vex.ui,org.eclipse.vex.ui.tests",
  org.eclipse.vex.core.internal.layout;x-friends:="org.eclipse.vex.ui,org.eclipse.vex.core.tests,org.eclipse.vex.ui.tests",
  org.eclipse.vex.core.internal.undo;x-friends:="org.eclipse.vex.core.tests,org.eclipse.vex.ui,org.eclipse.vex.ui.tests",
  org.eclipse.vex.core.internal.validator;x-friends:="org.eclipse.vex.ui,org.eclipse.vex.core.tests,org.eclipse.vex.ui.tests",
+ org.eclipse.vex.core.internal.visualization;x-friends:="org.eclipse.vex.ui,org.eclipse.vex.core.tests,org.eclipse.vex.ui.tests",
  org.eclipse.vex.core.internal.widget;x-friends:="org.eclipse.vex.ui,org.eclipse.vex.core.tests,org.eclipse.vex.ui.tests",
  org.eclipse.vex.core.internal.widget.swt;x-friends:="org.eclipse.vex.ui,org.eclipse.vex.core.tests,org.eclipse.vex.ui.tests",
  org.eclipse.vex.core.provisional.dom;uses:="org.eclipse.vex.core.internal.dom"
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/XML.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/XML.java
index 25967a9..31f3828 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/XML.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/XML.java
@@ -44,7 +44,7 @@
 		return c == 0x20 || c == 0x9 || c == 0xD || c == 0xA;

 	}

 

-	public static Pattern XML_WHITESPACE_PATTERN = Pattern.compile("[\\u0020\\u0009\\u000d\\u000a]");

+	public static final Pattern XML_WHITESPACE_PATTERN = Pattern.compile("[\\u0020\\u0009\\u000d\\u000a]");

 

 	/**

 	 * Replace runs of XML whitespace (see {@link #isWhitespace}) with a single space. Newlines in the input should be

diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/BaseBox.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/BaseBox.java
new file mode 100644
index 0000000..f41cab9
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/BaseBox.java
@@ -0,0 +1,63 @@
+/*******************************************************************************
+ * 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;
+
+import org.eclipse.vex.core.internal.core.Color;
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.LineStyle;
+
+/**
+ * @author Florian Thienel
+ */
+public abstract class BaseBox implements IBox {
+
+	@Override
+	public final boolean containsCoordinates(final int x, final int y) {
+		return containsX(x) && containsY(y);
+	}
+
+	@Override
+	public final boolean containsY(final int y) {
+		return !(isAbove(y) || isBelow(y));
+	}
+
+	@Override
+	public final boolean isAbove(final int y) {
+		return y >= getAbsoluteTop() + getHeight();
+	}
+
+	@Override
+	public final boolean isBelow(final int y) {
+		return y < getAbsoluteTop();
+	}
+
+	@Override
+	public final boolean containsX(final int x) {
+		return !(isRightOf(x) || isLeftOf(x));
+	}
+
+	@Override
+	public final boolean isRightOf(final int x) {
+		return x < getAbsoluteLeft();
+	}
+
+	@Override
+	public final boolean isLeftOf(final int x) {
+		return x >= getAbsoluteLeft() + getWidth();
+	}
+
+	protected final void drawDebugBounds(final Graphics graphics) {
+		graphics.setForeground(graphics.getColor(Color.BLACK));
+		graphics.setLineStyle(LineStyle.SOLID);
+		graphics.setLineWidth(1);
+		graphics.drawRect(0, 0, getWidth(), getHeight());
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/BaseBoxVisitor.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/BaseBoxVisitor.java
new file mode 100644
index 0000000..9575355
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/BaseBoxVisitor.java
@@ -0,0 +1,107 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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 class BaseBoxVisitor implements IBoxVisitor {
+
+	@Override
+	public void visit(final RootBox box) {
+		// ignore
+	}
+
+	@Override
+	public void visit(final VerticalBlock box) {
+		// ignore
+	}
+
+	@Override
+	public void visit(final StructuralFrame box) {
+		// ignore
+	}
+
+	@Override
+	public void visit(final StructuralNodeReference box) {
+		// ignore
+	}
+
+	@Override
+	public void visit(final HorizontalBar box) {
+		// ignore
+	}
+
+	@Override
+	public void visit(final List box) {
+		// ignore
+	}
+
+	@Override
+	public void visit(final ListItem box) {
+		// ignore
+	}
+
+	@Override
+	public void visit(final Paragraph box) {
+		// ignore
+	}
+
+	@Override
+	public void visit(final InlineNodeReference box) {
+		// ignore
+	}
+
+	@Override
+	public void visit(final InlineContainer box) {
+		// ignore
+	}
+
+	@Override
+	public void visit(final InlineFrame box) {
+		// ignore
+	}
+
+	@Override
+	public void visit(final StaticText box) {
+		// ignore
+	}
+
+	@Override
+	public void visit(final Image box) {
+		// ignore
+	}
+
+	@Override
+	public void visit(final TextContent box) {
+		// ignore
+	}
+
+	@Override
+	public void visit(final NodeEndOffsetPlaceholder box) {
+		// ignore
+	}
+
+	@Override
+	public void visit(final GraphicalBullet box) {
+		// ignore
+	}
+
+	@Override
+	public void visit(final Square box) {
+		// ignore
+	}
+
+	@Override
+	public void visit(final NodeTag box) {
+		// ignore
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/BaseBoxVisitorWithResult.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/BaseBoxVisitorWithResult.java
new file mode 100644
index 0000000..6364940
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/BaseBoxVisitorWithResult.java
@@ -0,0 +1,118 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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 class BaseBoxVisitorWithResult<T> implements IBoxVisitorWithResult<T> {
+
+	private final T defaultValue;
+
+	public BaseBoxVisitorWithResult() {
+		this(null);
+	}
+
+	public BaseBoxVisitorWithResult(final T defaultValue) {
+		this.defaultValue = defaultValue;
+	}
+
+	@Override
+	public T visit(final RootBox box) {
+		return defaultValue;
+	}
+
+	@Override
+	public T visit(final VerticalBlock box) {
+		return defaultValue;
+	}
+
+	@Override
+	public T visit(final StructuralFrame box) {
+		return defaultValue;
+	}
+
+	@Override
+	public T visit(final StructuralNodeReference box) {
+		return defaultValue;
+	}
+
+	@Override
+	public T visit(final HorizontalBar box) {
+		return defaultValue;
+	}
+
+	@Override
+	public T visit(final List box) {
+		return defaultValue;
+	}
+
+	@Override
+	public T visit(final ListItem box) {
+		return defaultValue;
+	}
+
+	@Override
+	public T visit(final Paragraph box) {
+		return defaultValue;
+	}
+
+	@Override
+	public T visit(final InlineNodeReference box) {
+		return defaultValue;
+	}
+
+	@Override
+	public T visit(final InlineContainer box) {
+		return defaultValue;
+	}
+
+	@Override
+	public T visit(final InlineFrame box) {
+		return defaultValue;
+	}
+
+	@Override
+	public T visit(final StaticText box) {
+		return defaultValue;
+	}
+
+	@Override
+	public T visit(final Image box) {
+		return defaultValue;
+	}
+
+	@Override
+	public T visit(final TextContent box) {
+		return defaultValue;
+	}
+
+	@Override
+	public T visit(final NodeEndOffsetPlaceholder box) {
+		return defaultValue;
+	}
+
+	@Override
+	public T visit(final GraphicalBullet box) {
+		return defaultValue;
+	}
+
+	@Override
+	public T visit(final Square box) {
+		return defaultValue;
+	}
+
+	@Override
+	public T visit(final NodeTag box) {
+		return defaultValue;
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/Border.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/Border.java
new file mode 100644
index 0000000..85494d5
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/Border.java
@@ -0,0 +1,106 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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 class Border {
+
+	public static final Border NULL = new Border(BorderLine.NULL);
+
+	public final BorderLine top;
+	public final BorderLine left;
+	public final BorderLine bottom;
+	public final BorderLine right;
+
+	public Border(final int size) {
+		this(size, size, size, size);
+	}
+
+	public Border(final int vertical, final int horizontal) {
+		this(vertical, horizontal, vertical, horizontal);
+	}
+
+	public Border(final int top, final int left, final int bottom, final int right) {
+		this(new BorderLine(top), new BorderLine(left), new BorderLine(bottom), new BorderLine(right));
+	}
+
+	public Border(final BorderLine border) {
+		this(border, border, border, border);
+	}
+
+	public Border(final BorderLine top, final BorderLine left, final BorderLine bottom, final BorderLine right) {
+		this.top = top;
+		this.left = left;
+		this.bottom = bottom;
+		this.right = right;
+	}
+
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = 1;
+		result = prime * result + (bottom == null ? 0 : bottom.hashCode());
+		result = prime * result + (left == null ? 0 : left.hashCode());
+		result = prime * result + (right == null ? 0 : right.hashCode());
+		result = prime * result + (top == null ? 0 : top.hashCode());
+		return result;
+	}
+
+	@Override
+	public boolean equals(final Object obj) {
+		if (this == obj) {
+			return true;
+		}
+		if (obj == null) {
+			return false;
+		}
+		if (getClass() != obj.getClass()) {
+			return false;
+		}
+		final Border other = (Border) obj;
+		if (bottom == null) {
+			if (other.bottom != null) {
+				return false;
+			}
+		} else if (!bottom.equals(other.bottom)) {
+			return false;
+		}
+		if (left == null) {
+			if (other.left != null) {
+				return false;
+			}
+		} else if (!left.equals(other.left)) {
+			return false;
+		}
+		if (right == null) {
+			if (other.right != null) {
+				return false;
+			}
+		} else if (!right.equals(other.right)) {
+			return false;
+		}
+		if (top == null) {
+			if (other.top != null) {
+				return false;
+			}
+		} else if (!top.equals(other.top)) {
+			return false;
+		}
+		return true;
+	}
+
+	@Override
+	public String toString() {
+		return "Border [top=" + top + ", left=" + left + ", bottom=" + bottom + ", right=" + right + "]";
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/BorderLine.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/BorderLine.java
new file mode 100644
index 0000000..c67f2db
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/BorderLine.java
@@ -0,0 +1,76 @@
+/*******************************************************************************
+ * 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;
+
+import org.eclipse.vex.core.internal.core.Color;
+import org.eclipse.vex.core.internal.core.LineStyle;
+
+public class BorderLine {
+
+	public static final BorderLine NULL = new BorderLine(0);
+
+	public final int width;
+	public final LineStyle style;
+	public final Color color;
+
+	public BorderLine(final int width) {
+		this(width, LineStyle.SOLID, Color.BLACK);
+	}
+
+	public BorderLine(final int width, final LineStyle style, final Color color) {
+		this.width = width;
+		this.style = style;
+		this.color = color;
+	}
+
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = 1;
+		result = prime * result + (color == null ? 0 : color.hashCode());
+		result = prime * result + (style == null ? 0 : style.hashCode());
+		result = prime * result + width;
+		return result;
+	}
+
+	@Override
+	public boolean equals(final Object obj) {
+		if (this == obj) {
+			return true;
+		}
+		if (obj == null) {
+			return false;
+		}
+		if (getClass() != obj.getClass()) {
+			return false;
+		}
+		final BorderLine other = (BorderLine) obj;
+		if (color == null) {
+			if (other.color != null) {
+				return false;
+			}
+		} else if (!color.equals(other.color)) {
+			return false;
+		}
+		if (style != other.style) {
+			return false;
+		}
+		if (width != other.width) {
+			return false;
+		}
+		return true;
+	}
+
+	@Override
+	public String toString() {
+		return "SingleBorder [width=" + width + ", style=" + style + ", color=" + color + "]";
+	}
+}
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
new file mode 100644
index 0000000..794dc0f
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/BoxFactory.java
@@ -0,0 +1,221 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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;
+
+import java.net.URL;
+
+import org.eclipse.vex.core.internal.boxes.NodeTag.Kind;
+import org.eclipse.vex.core.internal.core.Color;
+import org.eclipse.vex.core.internal.core.FontSpec;
+import org.eclipse.vex.core.internal.core.TextAlign;
+import org.eclipse.vex.core.internal.css.BulletStyle;
+import org.eclipse.vex.core.provisional.dom.ContentRange;
+import org.eclipse.vex.core.provisional.dom.IContent;
+import org.eclipse.vex.core.provisional.dom.INode;
+
+/**
+ * This factory allows the conventient creation of box structures using nested method calls to factory methods.
+ *
+ * @author Florian Thienel
+ */
+public class BoxFactory {
+
+	public static RootBox rootBox(final IStructuralBox... children) {
+		final RootBox rootBox = new RootBox();
+		for (final IStructuralBox child : children) {
+			rootBox.appendChild(child);
+		}
+		return rootBox;
+	}
+
+	public static VerticalBlock verticalBlock(final IStructuralBox... children) {
+		final VerticalBlock verticalBlock = new VerticalBlock();
+		for (final IStructuralBox child : children) {
+			verticalBlock.appendChild(child);
+		}
+		return verticalBlock;
+	}
+
+	public static StructuralFrame frame(final IStructuralBox component) {
+		final StructuralFrame frame = new StructuralFrame();
+		frame.setComponent(component);
+		return frame;
+	}
+
+	public static StructuralFrame frame(final IStructuralBox component, final Margin margin, final Border border, final Padding padding, final Color backgroundColor) {
+		final StructuralFrame frame = new StructuralFrame();
+		frame.setComponent(component);
+		frame.setMargin(margin);
+		frame.setBorder(border);
+		frame.setPadding(padding);
+		frame.setBackgroundColor(backgroundColor);
+		return frame;
+	}
+
+	public static InlineFrame frame(final IInlineBox component) {
+		final InlineFrame frame = new InlineFrame();
+		frame.setComponent(component);
+		return frame;
+	}
+
+	public static InlineFrame frame(final IInlineBox component, final Margin margin, final Border border, final Padding padding, final Color backgroundColor) {
+		final InlineFrame frame = new InlineFrame();
+		frame.setComponent(component);
+		frame.setMargin(margin);
+		frame.setBorder(border);
+		frame.setPadding(padding);
+		frame.setBackgroundColor(backgroundColor);
+		return frame;
+	}
+
+	public static StructuralNodeReference nodeReference(final INode node, final IStructuralBox component) {
+		final StructuralNodeReference structuralNodeReference = new StructuralNodeReference();
+		structuralNodeReference.setNode(node);
+		structuralNodeReference.setComponent(component);
+		return structuralNodeReference;
+	}
+
+	public static StructuralNodeReference nodeReferenceWithInlineContent(final INode node, final IStructuralBox component) {
+		final StructuralNodeReference structuralNodeReference = new StructuralNodeReference();
+		structuralNodeReference.setNode(node);
+		structuralNodeReference.setContainsInlineContent(true);
+		structuralNodeReference.setComponent(component);
+		return structuralNodeReference;
+	}
+
+	public static StructuralNodeReference nodeReferenceWithText(final INode node, final IStructuralBox component) {
+		final StructuralNodeReference structuralNodeReference = new StructuralNodeReference();
+		structuralNodeReference.setNode(node);
+		structuralNodeReference.setCanContainText(true);
+		structuralNodeReference.setContainsInlineContent(true);
+		structuralNodeReference.setComponent(component);
+		return structuralNodeReference;
+	}
+
+	public static InlineNodeReference nodeReference(final INode node, final IInlineBox component) {
+		final InlineNodeReference inlineNodeReference = new InlineNodeReference();
+		inlineNodeReference.setNode(node);
+		inlineNodeReference.setComponent(component);
+		return inlineNodeReference;
+	}
+
+	public static InlineNodeReference nodeReferenceWithText(final INode node, final IInlineBox component) {
+		final InlineNodeReference inlineNodeReference = new InlineNodeReference();
+		inlineNodeReference.setNode(node);
+		inlineNodeReference.setCanContainText(true);
+		inlineNodeReference.setComponent(component);
+		return inlineNodeReference;
+	}
+
+	public static HorizontalBar horizontalBar(final int height) {
+		final HorizontalBar horizontalBar = new HorizontalBar();
+		horizontalBar.setHeight(height);
+		return horizontalBar;
+	}
+
+	public static HorizontalBar horizontalBar(final int height, final Color color) {
+		final HorizontalBar horizontalBar = new HorizontalBar();
+		horizontalBar.setHeight(height);
+		horizontalBar.setColor(color);
+		return horizontalBar;
+	}
+
+	public static List list(final IStructuralBox component, final BulletStyle bulletStyle, final IBulletFactory bulletFactory) {
+		final List list = new List();
+		list.setBulletStyle(bulletStyle);
+		list.setBulletFactory(bulletFactory);
+		list.setComponent(component);
+		return list;
+	}
+
+	public static ListItem listItem(final IStructuralBox component) {
+		final ListItem listItem = new ListItem();
+		listItem.setComponent(component);
+		return listItem;
+	}
+
+	public static Paragraph paragraph(final IInlineBox... children) {
+		final Paragraph paragraph = new Paragraph();
+		for (final IInlineBox child : children) {
+			paragraph.appendChild(child);
+		}
+		return paragraph;
+	}
+
+	public static Paragraph paragraph(final TextAlign textAlign, final IInlineBox... children) {
+		final Paragraph paragraph = new Paragraph();
+		for (final IInlineBox child : children) {
+			paragraph.appendChild(child);
+		}
+		paragraph.setTextAlign(textAlign);
+		return paragraph;
+	}
+
+	public static InlineContainer inlineContainer(final IInlineBox... children) {
+		final InlineContainer inlineContainer = new InlineContainer();
+		for (final IInlineBox child : children) {
+			inlineContainer.appendChild(child);
+		}
+		return inlineContainer;
+	}
+
+	public static TextContent textContent(final IContent content, final ContentRange range, final FontSpec font, final Color color) {
+		final TextContent textContent = new TextContent();
+		textContent.setContent(content, range);
+		textContent.setFont(font);
+		textContent.setColor(color);
+		return textContent;
+	}
+
+	public static NodeEndOffsetPlaceholder endOffsetPlaceholder(final INode node, final FontSpec font) {
+		final NodeEndOffsetPlaceholder contentPlaceholder = new NodeEndOffsetPlaceholder();
+		contentPlaceholder.setNode(node);
+		contentPlaceholder.setFont(font);
+		return contentPlaceholder;
+	}
+
+	public static StaticText staticText(final String text, final FontSpec font, final Color color) {
+		final StaticText staticText = new StaticText();
+		staticText.setText(text);
+		staticText.setFont(font);
+		staticText.setColor(color);
+		return staticText;
+	}
+
+	public static Image image(final URL imageUrl) {
+		final Image image = new Image();
+		image.setImageUrl(imageUrl);
+		return image;
+	}
+
+	public static Square square(final int size, final Color color) {
+		final Square square = new Square();
+		square.setSize(size);
+		square.setColor(color);
+		return square;
+	}
+
+	public static GraphicalBullet graphicalBullet(final BulletStyle.Type type, final FontSpec font, final Color color) {
+		final GraphicalBullet bullet = new GraphicalBullet();
+		bullet.setType(type);
+		bullet.setFont(font);
+		bullet.setColor(color);
+		return bullet;
+	}
+
+	public static NodeTag nodeTag(final Kind kind, final INode node, final Color foreground) {
+		final NodeTag nodeTag = new NodeTag();
+		nodeTag.setKind(kind);
+		nodeTag.setNode(node);
+		nodeTag.setColor(foreground);
+		return nodeTag;
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/CharSequenceSplitter.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/CharSequenceSplitter.java
new file mode 100644
index 0000000..f66c0df
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/CharSequenceSplitter.java
@@ -0,0 +1,111 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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;
+
+import org.eclipse.vex.core.XML;
+import org.eclipse.vex.core.internal.core.Graphics;
+
+/**
+ * @author Florian Thienel
+ */
+public class CharSequenceSplitter {
+
+	private CharSequence charSequence;
+	private int startPosition;
+	private int endPosition;
+
+	public void setContent(final CharSequence charSequence) {
+		setContent(charSequence, 0, charSequence.length() - 1);
+	}
+
+	public void setContent(final CharSequence charSequence, final int startPosition, final int endPosition) {
+		this.charSequence = charSequence;
+		this.startPosition = startPosition;
+		this.endPosition = endPosition;
+	}
+
+	private int textLength() {
+		return endPosition - startPosition + 1;
+	}
+
+	private String substring(final int beginIndex, final int endIndex) {
+		return charSequence.subSequence(startPosition + beginIndex, startPosition + endIndex).toString();
+	}
+
+	private char charAt(final int position) {
+		return charSequence.charAt(startPosition + position);
+	}
+
+	public int findSplittingPositionBefore(final Graphics graphics, final int x, final int maxWidth, final boolean force) {
+		final int positionAtWidth = findPositionAfter(graphics, x, maxWidth) - 1;
+		final int properSplittingPosition = findProperSplittingPositionBefore(positionAtWidth);
+
+		if (textLength() > properSplittingPosition + 2 && isSplittingCharacter(properSplittingPosition + 1) && !isSplittingCharacter(properSplittingPosition + 2)) {
+			return properSplittingPosition + 2;
+		}
+		if (textLength() == properSplittingPosition + 2 && isSplittingCharacter(properSplittingPosition + 1)) {
+			return properSplittingPosition + 2;
+		}
+		if (properSplittingPosition == -1 && force) {
+			return positionAtWidth + 1;
+		}
+		return properSplittingPosition + 1;
+	}
+
+	private int findPositionAfter(final Graphics graphics, final int x, final int maxWidth) {
+		if (x < 0) {
+			return 0;
+		}
+		if (x >= maxWidth) {
+			return textLength();
+		}
+
+		int begin = 0;
+		int end = textLength();
+		int pivot = guessPositionAt(x, maxWidth);
+		while (begin < end - 1) {
+			final int textWidth = stringWidthBeforeOffset(graphics, pivot);
+			if (textWidth > x) {
+				end = pivot;
+			} else if (textWidth < x) {
+				begin = pivot;
+			} else {
+				return pivot;
+			}
+			pivot = (begin + end) / 2;
+		}
+
+		return pivot;
+	}
+
+	private int guessPositionAt(final int x, final int maxWidth) {
+		final float splittingRatio = (float) x / maxWidth;
+		return Math.round(splittingRatio * textLength());
+	}
+
+	private int stringWidthBeforeOffset(final Graphics graphics, final int offset) {
+		return graphics.stringWidth(substring(0, offset));
+	}
+
+	private int findProperSplittingPositionBefore(final int position) {
+		for (int i = Math.min(position, textLength() - 1); i >= 0; i -= 1) {
+			if (isSplittingCharacter(i)) {
+				return i;
+			}
+		}
+		return -1;
+	}
+
+	private boolean isSplittingCharacter(final int position) {
+		return XML.isWhitespace(charAt(position));
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/ChildBoxPainter.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/ChildBoxPainter.java
new file mode 100644
index 0000000..7876f44
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/ChildBoxPainter.java
@@ -0,0 +1,73 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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;
+
+import java.util.List;
+
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+
+/**
+ * @author Florian Thienel
+ */
+public class ChildBoxPainter {
+
+	public static void paint(final List<? extends IChildBox> children, final Graphics graphics) {
+		if (children.isEmpty()) {
+			return;
+		}
+		final Rectangle clipBounds = graphics.getClipBounds();
+		int i = findIndexOfFirstVisibleChild(children, clipBounds);
+		while (i < children.size()) {
+			final IChildBox child = children.get(i);
+			if (!child.getBounds().intersects(clipBounds)) {
+				break;
+			}
+
+			paint(child, graphics);
+
+			i += 1;
+		}
+	}
+
+	public static void paint(final IChildBox child, final Graphics graphics) {
+		graphics.moveOrigin(child.getLeft(), child.getTop());
+		child.paint(graphics);
+		graphics.moveOrigin(-child.getLeft(), -child.getTop());
+	}
+
+	private static int findIndexOfFirstVisibleChild(final List<? extends IChildBox> children, final Rectangle clipBounds) {
+		int lowerBound = 0;
+		int upperBound = children.size() - 1;
+
+		while (upperBound - lowerBound > 1) {
+			final int pivotIndex = center(lowerBound, upperBound);
+			final Rectangle pivotBounds = children.get(pivotIndex).getBounds();
+
+			if (pivotBounds.below(clipBounds)) {
+				upperBound = pivotIndex;
+			} else if (pivotBounds.above(clipBounds)) {
+				lowerBound = pivotIndex;
+			} else {
+				upperBound = pivotIndex;
+			}
+		}
+
+		if (children.get(lowerBound).getBounds().intersects(clipBounds)) {
+			return lowerBound;
+		}
+		return upperBound;
+	}
+
+	private static int center(final int lowerBound, final int upperBound) {
+		return lowerBound + (upperBound - lowerBound) / 2;
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/DepthFirstBoxTraversal.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/DepthFirstBoxTraversal.java
new file mode 100644
index 0000000..6fcea6b
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/DepthFirstBoxTraversal.java
@@ -0,0 +1,82 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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 abstract class DepthFirstBoxTraversal<T> extends BaseBoxVisitorWithResult<T> {
+
+	public DepthFirstBoxTraversal() {
+		super(null);
+	}
+
+	@Override
+	public T visit(final RootBox box) {
+		return traverseChildren(box);
+	}
+
+	@Override
+	public T visit(final VerticalBlock box) {
+		return traverseChildren(box);
+	}
+
+	@Override
+	public T visit(final StructuralFrame box) {
+		return box.getComponent().accept(this);
+	}
+
+	@Override
+	public T visit(final StructuralNodeReference box) {
+		return box.getComponent().accept(this);
+	}
+
+	@Override
+	public T visit(final List box) {
+		return box.getComponent().accept(this);
+	}
+
+	@Override
+	public T visit(final ListItem box) {
+		return box.getComponent().accept(this);
+	}
+
+	@Override
+	public T visit(final Paragraph box) {
+		return traverseChildren(box);
+	}
+
+	@Override
+	public T visit(final InlineNodeReference box) {
+		return box.getComponent().accept(this);
+	}
+
+	@Override
+	public T visit(final InlineContainer box) {
+		return traverseChildren(box);
+	}
+
+	@Override
+	public T visit(final InlineFrame box) {
+		return box.getComponent().accept(this);
+	}
+
+	protected final <C extends IBox> T traverseChildren(final IParentBox<C> box) {
+		for (final C child : box.getChildren()) {
+			final T childResult = child.accept(this);
+
+			if (childResult != null) {
+				return childResult;
+			}
+		}
+		return null;
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/GraphicalBullet.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/GraphicalBullet.java
new file mode 100644
index 0000000..5db640b
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/GraphicalBullet.java
@@ -0,0 +1,117 @@
+/*******************************************************************************
+ * Copyright (c) 2016 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;
+
+import org.eclipse.vex.core.internal.core.Color;
+import org.eclipse.vex.core.internal.core.FontResource;
+import org.eclipse.vex.core.internal.core.FontSpec;
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.css.BulletStyle;
+
+/**
+ * @author Florian Thienel
+ */
+public class GraphicalBullet extends SimpleInlineBox {
+
+	private static final float HEIGHT_RATIO = 0.7f;
+	private static final float LIFT_RATIO = 0.15f;
+
+	private int width;
+	private int height;
+
+	private BulletStyle.Type bulletType;
+	private FontSpec fontSpec;
+	private Color color;
+
+	@Override
+	public int getWidth() {
+		return width;
+	}
+
+	@Override
+	public int getHeight() {
+		return height;
+	}
+
+	@Override
+	public int getBaseline() {
+		return height;
+	}
+
+	public void accept(final IBoxVisitor visitor) {
+		visitor.visit(this);
+	}
+
+	@Override
+	public <T> T accept(final IBoxVisitorWithResult<T> visitor) {
+		return visitor.visit(this);
+	}
+
+	public void setType(final BulletStyle.Type bulletType) {
+		this.bulletType = bulletType;
+	}
+
+	public void setFont(final FontSpec fontSpec) {
+		this.fontSpec = fontSpec;
+	}
+
+	public void setColor(final Color color) {
+		this.color = color;
+	}
+
+	@Override
+	public void layout(final Graphics graphics) {
+		if (fontSpec == null) {
+			return;
+		}
+
+		applyFont(graphics);
+		final int ascent = graphics.getFontMetrics().getAscent();
+		height = Math.round(ascent * HEIGHT_RATIO);
+		final int lift = Math.round(ascent * LIFT_RATIO);
+		width = height - lift;
+	}
+
+	private void applyFont(final Graphics graphics) {
+		final FontResource font = graphics.getFont(fontSpec);
+		graphics.setCurrentFont(font);
+	}
+
+	@Override
+	public boolean reconcileLayout(final Graphics graphics) {
+		final int oldHeight = height;
+
+		layout(graphics);
+
+		return height != oldHeight;
+	}
+
+	@Override
+	public void paint(final Graphics graphics) {
+		graphics.setColor(graphics.getColor(color));
+
+		switch (bulletType) {
+		case SQUARE:
+			graphics.fillRect(0, 0, width, width);
+			break;
+		case DISC:
+			graphics.fillOval(0, 0, width, width);
+			break;
+		case CIRCLE:
+			graphics.setLineWidth(1);
+			graphics.drawOval(0, 0, width, width);
+			break;
+		default:
+			graphics.fillRect(0, 0, width, width);
+			break;
+		}
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/HorizontalBar.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/HorizontalBar.java
new file mode 100644
index 0000000..7c89188
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/HorizontalBar.java
@@ -0,0 +1,130 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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;
+
+import org.eclipse.vex.core.internal.core.Color;
+import org.eclipse.vex.core.internal.core.ColorResource;
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+
+/**
+ * @author Florian Thienel
+ */
+public class HorizontalBar extends BaseBox implements IStructuralBox {
+
+	private IBox parent;
+	private int top;
+	private int left;
+	private int width;
+	private int height;
+	private Color color;
+
+	@Override
+	public void setParent(final IBox parent) {
+		this.parent = parent;
+	}
+
+	@Override
+	public IBox getParent() {
+		return parent;
+	}
+
+	@Override
+	public int getAbsoluteTop() {
+		if (parent == null) {
+			return top;
+		}
+		return parent.getAbsoluteTop() + top;
+	}
+
+	@Override
+	public int getAbsoluteLeft() {
+		if (parent == null) {
+			return left;
+		}
+		return parent.getAbsoluteLeft() + left;
+	}
+
+	@Override
+	public int getTop() {
+		return top;
+	}
+
+	@Override
+	public int getLeft() {
+		return left;
+	}
+
+	@Override
+	public void setPosition(final int top, final int left) {
+		this.top = top;
+		this.left = left;
+	}
+
+	@Override
+	public int getWidth() {
+		return width;
+	}
+
+	@Override
+	public void setWidth(final int width) {
+		this.width = Math.max(0, width);
+	}
+
+	@Override
+	public int getHeight() {
+		return height;
+	}
+
+	public void setHeight(final int height) {
+		this.height = height;
+	}
+
+	@Override
+	public Rectangle getBounds() {
+		return new Rectangle(left, top, width, height);
+	}
+
+	public Color getColor() {
+		return color;
+	}
+
+	public void setColor(final Color color) {
+		this.color = color;
+	}
+
+	@Override
+	public void accept(final IBoxVisitor visitor) {
+		visitor.visit(this);
+	}
+
+	@Override
+	public <T> T accept(final IBoxVisitorWithResult<T> visitor) {
+		return visitor.visit(this);
+	}
+
+	public void layout(final Graphics graphics) {
+		// ignore, everything is static
+	}
+
+	@Override
+	public boolean reconcileLayout(final Graphics graphics) {
+		// ignore, everything is static
+		return false;
+	}
+
+	@Override
+	public void paint(final Graphics graphics) {
+		final ColorResource colorResource = graphics.getColor(color);
+		graphics.setColor(colorResource);
+		graphics.fillRect(0, 0, width, height);
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IBox.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IBox.java
new file mode 100644
index 0000000..1592677
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IBox.java
@@ -0,0 +1,58 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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;
+
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+
+/**
+ * @author Florian Thienel
+ */
+public interface IBox {
+
+	int getAbsoluteTop();
+
+	int getAbsoluteLeft();
+
+	int getTop();
+
+	int getLeft();
+
+	int getWidth();
+
+	int getHeight();
+
+	Rectangle getBounds();
+
+	void accept(IBoxVisitor visitor);
+
+	<T> T accept(IBoxVisitorWithResult<T> visitor);
+
+	void layout(Graphics graphics);
+
+	boolean reconcileLayout(Graphics graphics);
+
+	void paint(Graphics graphics);
+
+	public abstract boolean isLeftOf(final int x);
+
+	public abstract boolean isRightOf(final int x);
+
+	public abstract boolean containsX(final int x);
+
+	public abstract boolean isBelow(final int y);
+
+	public abstract boolean isAbove(final int y);
+
+	public abstract boolean containsY(final int y);
+
+	public abstract boolean containsCoordinates(final int x, final int y);
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IBoxVisitor.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IBoxVisitor.java
new file mode 100644
index 0000000..be3bc01
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IBoxVisitor.java
@@ -0,0 +1,53 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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 interface IBoxVisitor {
+
+	void visit(RootBox box);
+
+	void visit(VerticalBlock box);
+
+	void visit(StructuralFrame box);
+
+	void visit(StructuralNodeReference box);
+
+	void visit(HorizontalBar box);
+
+	void visit(List box);
+
+	void visit(ListItem box);
+
+	void visit(Paragraph box);
+
+	void visit(InlineNodeReference box);
+
+	void visit(InlineContainer box);
+
+	void visit(InlineFrame box);
+
+	void visit(StaticText box);
+
+	void visit(Image box);
+
+	void visit(TextContent box);
+
+	void visit(NodeEndOffsetPlaceholder box);
+
+	void visit(GraphicalBullet box);
+
+	void visit(Square box);
+
+	void visit(NodeTag box);
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IBoxVisitorWithResult.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IBoxVisitorWithResult.java
new file mode 100644
index 0000000..1a01ea0
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IBoxVisitorWithResult.java
@@ -0,0 +1,53 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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 interface IBoxVisitorWithResult<T> {
+
+	T visit(RootBox box);
+
+	T visit(VerticalBlock box);
+
+	T visit(StructuralFrame box);
+
+	T visit(StructuralNodeReference box);
+
+	T visit(HorizontalBar box);
+
+	T visit(List box);
+
+	T visit(ListItem box);
+
+	T visit(Paragraph box);
+
+	T visit(InlineNodeReference box);
+
+	T visit(InlineContainer box);
+
+	T visit(InlineFrame box);
+
+	T visit(StaticText box);
+
+	T visit(Image box);
+
+	T visit(TextContent box);
+
+	T visit(NodeEndOffsetPlaceholder box);
+
+	T visit(GraphicalBullet box);
+
+	T visit(Square box);
+
+	T visit(NodeTag box);
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IBulletFactory.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IBulletFactory.java
new file mode 100644
index 0000000..d7f6ee1
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IBulletFactory.java
@@ -0,0 +1,20 @@
+/*******************************************************************************
+ * Copyright (c) 2016 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;
+
+import org.eclipse.vex.core.internal.css.BulletStyle;
+
+/**
+ * @author Florian Thienel
+ */
+public interface IBulletFactory {
+	IInlineBox createBullet(BulletStyle style, int itemIndex, int itemCount);
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IChildBox.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IChildBox.java
new file mode 100644
index 0000000..ecaee28
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IChildBox.java
@@ -0,0 +1,22 @@
+/*******************************************************************************
+ * 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 interface IChildBox extends IBox {
+
+	void setParent(IBox parent);
+
+	IBox getParent();
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IContentBox.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IContentBox.java
new file mode 100644
index 0000000..e76b0a8
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IContentBox.java
@@ -0,0 +1,44 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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;
+
+import org.eclipse.vex.core.internal.core.Color;
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+import org.eclipse.vex.core.provisional.dom.ContentRange;
+import org.eclipse.vex.core.provisional.dom.IContent;
+
+/**
+ * @author Florian Thienel
+ */
+public interface IContentBox extends IChildBox {
+
+	IContent getContent();
+
+	int getStartOffset();
+
+	int getEndOffset();
+
+	ContentRange getRange();
+
+	boolean isEmpty();
+
+	boolean isAtStart(int offset);
+
+	boolean isAtEnd(int offset);
+
+	Rectangle getPositionArea(Graphics graphics, int offset);
+
+	int getOffsetForCoordinates(Graphics graphics, int x, int y);
+
+	void highlight(Graphics graphics, Color foreground, Color background);
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IDecoratorBox.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IDecoratorBox.java
new file mode 100644
index 0000000..10bf5bd
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IDecoratorBox.java
@@ -0,0 +1,21 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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 interface IDecoratorBox<T extends IBox> extends IBox {
+
+	void setComponent(T component);
+
+	T getComponent();
+}
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
new file mode 100644
index 0000000..a5aec4a
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IInlineBox.java
@@ -0,0 +1,49 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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;
+
+import org.eclipse.vex.core.internal.core.Graphics;
+
+/**
+ * @author Florian Thienel
+ */
+public interface IInlineBox extends IChildBox {
+
+	void setPosition(int top, int left);
+
+	/**
+	 * The baseline is relative to the top of this box.
+	 */
+	int getBaseline();
+
+	int getMaxWidth();
+
+	void setMaxWidth(int width);
+
+	int getInvisibleGapAtStart(Graphics graphics);
+
+	int getInvisibleGapAtEnd(Graphics graphics);
+
+	LineWrappingRule getLineWrappingAtStart();
+
+	LineWrappingRule getLineWrappingAtEnd();
+
+	boolean requiresSplitForLineWrapping();
+
+	boolean canJoin(IInlineBox other);
+
+	boolean join(IInlineBox other);
+
+	boolean canSplit();
+
+	IInlineBox splitTail(Graphics graphics, int headWidth, boolean force);
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IParentBox.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IParentBox.java
new file mode 100644
index 0000000..36643a2
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IParentBox.java
@@ -0,0 +1,30 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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;
+
+import java.util.Collection;
+
+/**
+ * @author Florian Thienel
+ */
+public interface IParentBox<T extends IBox> extends IBox {
+
+	boolean hasChildren();
+
+	void prependChild(T child);
+
+	void appendChild(T child);
+
+	void replaceChildren(Collection<? extends IBox> oldChildren, T newChild);
+
+	Iterable<T> getChildren();
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IStructuralBox.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IStructuralBox.java
new file mode 100644
index 0000000..5187805
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IStructuralBox.java
@@ -0,0 +1,28 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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;
+
+import org.eclipse.vex.core.internal.core.Rectangle;
+
+/**
+ * @author Florian Thienel
+ */
+public interface IStructuralBox extends IChildBox {
+
+	void setPosition(int top, int left);
+
+	void setWidth(int width);
+
+	/**
+	 * The bounds are always relative to the parent box.
+	 */
+	Rectangle getBounds();
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/Image.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/Image.java
new file mode 100644
index 0000000..9a0623e
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/Image.java
@@ -0,0 +1,258 @@
+/*******************************************************************************
+ * Copyright (c) 2016 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;
+
+import java.net.URL;
+
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Length;
+import org.eclipse.vex.core.internal.core.Point;
+import org.eclipse.vex.core.internal.core.Rectangle;
+
+public class Image extends BaseBox implements IInlineBox {
+
+	private IBox parent;
+	private int top;
+	private int left;
+	private int width;
+	private int height;
+	private int maxWidth;
+	private LineWrappingRule lineWrappingAtStart;
+	private LineWrappingRule lineWrappingAtEnd;
+
+	private URL imageUrl;
+	private Length preferredWidth;
+	private Length preferredHeight;
+
+	private org.eclipse.vex.core.internal.core.Image image; // TODO use a cache for the actual image data
+
+	private boolean layoutValid;
+
+	@Override
+	public void setParent(final IBox parent) {
+		this.parent = parent;
+	}
+
+	@Override
+	public IBox getParent() {
+		return parent;
+	}
+
+	@Override
+	public int getAbsoluteTop() {
+		if (parent == null) {
+			return top;
+		}
+		return parent.getAbsoluteTop() + top;
+	}
+
+	@Override
+	public int getAbsoluteLeft() {
+		if (parent == null) {
+			return left;
+		}
+		return parent.getAbsoluteLeft() + left;
+	}
+
+	@Override
+	public int getTop() {
+		return top;
+	}
+
+	@Override
+	public int getLeft() {
+		return left;
+	}
+
+	@Override
+	public void setPosition(final int top, final int left) {
+		this.top = top;
+		this.left = left;
+	}
+
+	@Override
+	public int getWidth() {
+		return width;
+	}
+
+	@Override
+	public int getHeight() {
+		return height;
+	}
+
+	@Override
+	public Rectangle getBounds() {
+		return new Rectangle(left, top, width, height);
+	}
+
+	@Override
+	public int getBaseline() {
+		return height;
+	}
+
+	@Override
+	public int getMaxWidth() {
+		return maxWidth;
+	}
+
+	@Override
+	public void setMaxWidth(final int maxWidth) {
+		this.maxWidth = maxWidth;
+		layoutValid = false;
+	}
+
+	@Override
+	public int getInvisibleGapAtStart(final Graphics graphics) {
+		return 0;
+	}
+
+	@Override
+	public int getInvisibleGapAtEnd(final Graphics graphics) {
+		return 0;
+	}
+
+	@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 URL getImageUrl() {
+		return imageUrl;
+	}
+
+	public void setImageUrl(final URL imageUrl) {
+		this.imageUrl = imageUrl;
+		layoutValid = false;
+	}
+
+	public Length getPreferredWidth() {
+		return preferredWidth;
+	}
+
+	public void setPreferredWidth(final Length preferredWidth) {
+		this.preferredWidth = preferredWidth;
+		layoutValid = false;
+	}
+
+	public Length getPreferredHeight() {
+		return preferredHeight;
+	}
+
+	public void setPreferredHeight(final Length preferredHeight) {
+		this.preferredHeight = preferredHeight;
+		layoutValid = false;
+	}
+
+	@Override
+	public void accept(final IBoxVisitor visitor) {
+		visitor.visit(this);
+	}
+
+	@Override
+	public <T> T accept(final IBoxVisitorWithResult<T> visitor) {
+		return visitor.visit(this);
+	}
+
+	@Override
+	public void layout(final Graphics graphics) {
+		if (layoutValid) {
+			return;
+		}
+
+		image = graphics.getImage(imageUrl);
+		final Point dimensions = calculateActualDimensions();
+
+		width = dimensions.getX();
+		height = dimensions.getY();
+
+		layoutValid = true;
+	}
+
+	private Point calculateActualDimensions() {
+		final int width = preferredWidth == null ? 0 : preferredWidth.get(maxWidth);
+		final int height = preferredHeight == null ? 0 : preferredHeight.get(image.getHeight());
+		if (width != 0 && height != 0) {
+			return new Point(width, height);
+		}
+		if (width == 0 && height != 0) {
+			return new Point(scale(image.getWidth(), image.getHeight(), height), height);
+		}
+		if (width != 0 && height == 0) {
+			return new Point(width, scale(image.getHeight(), image.getWidth(), width));
+		}
+		return ensureMaxWidthNotExceeded(new Point(image.getWidth(), image.getHeight()));
+	}
+
+	private Point ensureMaxWidthNotExceeded(final Point dimensions) {
+		if (maxWidth > 0 && dimensions.getX() > maxWidth) {
+			return new Point(maxWidth, scale(dimensions.getY(), dimensions.getX(), maxWidth));
+		}
+		return dimensions;
+	}
+
+	private static int scale(final int opposite, final int current, final int scaled) {
+		return Math.round(1f * scaled / current * opposite);
+	}
+
+	@Override
+	public boolean reconcileLayout(final Graphics graphics) {
+		final int oldHeight = height;
+		final int oldWidth = width;
+
+		layout(graphics);
+
+		return oldHeight != height || oldWidth != width;
+	}
+
+	@Override
+	public void paint(final Graphics graphics) {
+		graphics.drawImage(image, 0, 0, width, height);
+	}
+
+	@Override
+	public boolean canJoin(final IInlineBox other) {
+		return false;
+	}
+
+	@Override
+	public boolean join(final IInlineBox other) {
+		return false;
+	}
+
+	@Override
+	public boolean canSplit() {
+		return false;
+	}
+
+	@Override
+	public IInlineBox splitTail(final Graphics graphics, final int headWidth, final boolean force) {
+		throw new UnsupportedOperationException("Image cannot be split!");
+	}
+
+}
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
new file mode 100644
index 0000000..ffcee5d
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/InlineContainer.java
@@ -0,0 +1,383 @@
+/*******************************************************************************
+ * 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;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.ListIterator;
+
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+
+public class InlineContainer extends BaseBox implements IInlineBox, IParentBox<IInlineBox> {
+
+	private IBox parent;
+	private int top;
+	private int left;
+	private int width;
+	private int height;
+	private int baseline;
+	private int maxWidth;
+	private boolean containsChildThatRequiresLineWrapping;
+
+	private final LinkedList<IInlineBox> children = new LinkedList<IInlineBox>();
+
+	@Override
+	public void setParent(final IBox parent) {
+		this.parent = parent;
+	}
+
+	@Override
+	public IBox getParent() {
+		return parent;
+	}
+
+	@Override
+	public int getAbsoluteTop() {
+		if (parent == null) {
+			return top;
+		}
+		return parent.getAbsoluteTop() + top;
+	}
+
+	@Override
+	public int getAbsoluteLeft() {
+		if (parent == null) {
+			return left;
+		}
+		return parent.getAbsoluteLeft() + left;
+	}
+
+	public int getTop() {
+		return top;
+	}
+
+	public int getLeft() {
+		return left;
+	}
+
+	public void setPosition(final int top, final int left) {
+		this.top = top;
+		this.left = left;
+	}
+
+	@Override
+	public int getWidth() {
+		return width;
+	}
+
+	@Override
+	public int getHeight() {
+		return height;
+	}
+
+	@Override
+	public Rectangle getBounds() {
+		return new Rectangle(left, top, width, height);
+	}
+
+	@Override
+	public int getBaseline() {
+		return baseline;
+	}
+
+	@Override
+	public int getMaxWidth() {
+		return maxWidth;
+	}
+
+	@Override
+	public void setMaxWidth(final int maxWidth) {
+		this.maxWidth = maxWidth;
+	}
+
+	@Override
+	public int getInvisibleGapAtStart(final Graphics graphics) {
+		if (children.isEmpty()) {
+			return 0;
+		}
+		return children.getFirst().getInvisibleGapAtStart(graphics);
+	}
+
+	@Override
+	public int getInvisibleGapAtEnd(final Graphics graphics) {
+		if (children.isEmpty()) {
+			return 0;
+		}
+		return children.getLast().getInvisibleGapAtEnd(graphics);
+	}
+
+	@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);
+	}
+
+	@Override
+	public <T> T accept(final IBoxVisitorWithResult<T> visitor) {
+		return visitor.visit(this);
+	}
+
+	@Override
+	public boolean hasChildren() {
+		return !children.isEmpty();
+	}
+
+	@Override
+	public void prependChild(final IInlineBox child) {
+		if (child == null) {
+			return;
+		}
+		if (!joinWithFirstChild(child)) {
+			child.setParent(this);
+			children.addFirst(child);
+		}
+	}
+
+	private boolean joinWithFirstChild(final IInlineBox box) {
+		if (!hasChildren()) {
+			return false;
+		}
+		final IInlineBox firstChild = children.getFirst();
+		final boolean joined = box.join(firstChild);
+		if (joined) {
+			children.removeFirst();
+			children.addFirst(box);
+		}
+		return joined;
+	}
+
+	@Override
+	public void appendChild(final IInlineBox child) {
+		if (child == null) {
+			return;
+		}
+		if (!joinWithLastChild(child)) {
+			child.setParent(this);
+			children.addLast(child);
+		}
+	}
+
+	private boolean joinWithLastChild(final IInlineBox box) {
+		if (!hasChildren()) {
+			return false;
+		}
+		final IInlineBox lastChild = children.getLast();
+		final boolean joined = lastChild.join(box);
+		return joined;
+	}
+
+	@Override
+	public void replaceChildren(final Collection<? extends IBox> oldChildren, final IInlineBox newChild) {
+		boolean newChildInserted = false;
+
+		for (final ListIterator<IInlineBox> iter = children.listIterator(); iter.hasNext();) {
+			final IInlineBox child = iter.next();
+			if (oldChildren.contains(child)) {
+				iter.remove();
+				if (!newChildInserted) {
+					iter.add(newChild);
+					newChild.setParent(this);
+					newChildInserted = true;
+				}
+			}
+		}
+	}
+
+	@Override
+	public Iterable<IInlineBox> getChildren() {
+		return children;
+	}
+
+	@Override
+	public void layout(final Graphics graphics) {
+		layoutChildren(graphics);
+		calculateBoundsAndBaseline();
+		arrangeChildrenOnBaseline();
+		updateRequiresSplitForLineWrapping();
+	}
+
+	private void layoutChildren(final Graphics graphics) {
+		for (final IInlineBox child : children) {
+			child.setMaxWidth(maxWidth);
+			child.layout(graphics);
+			containsChildThatRequiresLineWrapping |= child.requiresSplitForLineWrapping();
+		}
+	}
+
+	private void calculateBoundsAndBaseline() {
+		width = 0;
+		height = 0;
+		baseline = 0;
+		int descend = 0;
+		for (final IInlineBox child : children) {
+			width += child.getWidth();
+			descend = Math.max(descend, child.getHeight() - child.getBaseline());
+			baseline = Math.max(baseline, child.getBaseline());
+		}
+		height = baseline + descend;
+	}
+
+	private void arrangeChildrenOnBaseline() {
+		int childLeft = 0;
+		for (final IInlineBox child : children) {
+			final int childTop = baseline - child.getBaseline();
+			child.setPosition(childTop, childLeft);
+			childLeft += child.getWidth();
+		}
+	}
+
+	private void updateRequiresSplitForLineWrapping() {
+		containsChildThatRequiresLineWrapping = false;
+		for (final IInlineBox child : children) {
+			containsChildThatRequiresLineWrapping |= child.requiresSplitForLineWrapping();
+		}
+	}
+
+	@Override
+	public boolean reconcileLayout(final Graphics graphics) {
+		final int oldWidth = width;
+		final int oldHeight = height;
+		final int oldBaseline = baseline;
+		calculateBoundsAndBaseline();
+		return oldWidth != width || oldHeight != height || oldBaseline != baseline;
+	}
+
+	@Override
+	public void paint(final Graphics graphics) {
+		ChildBoxPainter.paint(children, graphics);
+	}
+
+	@Override
+	public boolean canJoin(final IInlineBox other) {
+		if (!(other instanceof InlineContainer)) {
+			return false;
+		}
+		return true;
+	}
+
+	@Override
+	public boolean join(final IInlineBox other) {
+		if (!canJoin(other)) {
+			return false;
+		}
+		final InlineContainer otherInlineContainer = (InlineContainer) other;
+
+		for (int i = 0; i < otherInlineContainer.children.size(); i += 1) {
+			final IInlineBox child = otherInlineContainer.children.get(i);
+			appendChild(child);
+		}
+
+		calculateBoundsAndBaseline();
+		arrangeChildrenOnBaseline();
+
+		return true;
+	}
+
+	@Override
+	public boolean canSplit() {
+		if (children.isEmpty()) {
+			return false;
+		}
+		if (children.size() == 1) {
+			return children.getFirst().canSplit();
+		}
+		return true;
+	}
+
+	@Override
+	public InlineContainer splitTail(final Graphics graphics, final int headWidth, final boolean force) {
+		final int splitIndex = findChildIndexToSplitAt(headWidth);
+		if (splitIndex == -1) {
+			return new InlineContainer();
+		}
+
+		final IInlineBox splitChild = children.get(splitIndex);
+
+		final IInlineBox splitChildTail;
+		if (splitChild.canSplit()) {
+			splitChildTail = splitChild.splitTail(graphics, headWidth - splitChild.getLeft(), force && splitIndex == 0);
+			if (splitChild.getWidth() == 0) {
+				children.remove(splitChild);
+				splitChild.setParent(null);
+			}
+		} else {
+			splitChildTail = splitChild;
+		}
+
+		final InlineContainer tail = new InlineContainer();
+		tail.setParent(parent);
+
+		if (splitChildTail.getWidth() > 0 && splitChild != splitChildTail) {
+			tail.appendChild(splitChildTail);
+		}
+
+		if (splitChild.getWidth() == 0 || splitChild == splitChildTail) {
+			moveChildrenTo(tail, splitIndex);
+		} else {
+			moveChildrenTo(tail, splitIndex + 1);
+		}
+
+		layout(graphics);
+		tail.layout(graphics);
+
+		return tail;
+	}
+
+	private int findChildIndexToSplitAt(final int headWidth) {
+		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;
+	}
+
+	private void moveChildrenTo(final InlineContainer destination, final int startIndex) {
+		while (startIndex < children.size()) {
+			final IInlineBox child = children.get(startIndex);
+			children.remove(startIndex);
+			destination.appendChild(child);
+		}
+	}
+
+}
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
new file mode 100644
index 0000000..2cad78a
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/InlineFrame.java
@@ -0,0 +1,369 @@
+/*******************************************************************************
+ * 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;
+
+import org.eclipse.vex.core.internal.core.Color;
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+
+public class InlineFrame extends BaseBox implements IInlineBox, IDecoratorBox<IInlineBox> {
+
+	private IBox parent;
+	private int top;
+	private int left;
+	private int width;
+	private int height;
+	private int maxWidth;
+
+	private Margin margin = Margin.NULL;
+	private Border border = Border.NULL;
+	private Padding padding = Padding.NULL;
+	private Color backgroundColor = null;
+
+	private IInlineBox component;
+
+	@Override
+	public void setParent(final IBox parent) {
+		this.parent = parent;
+	}
+
+	@Override
+	public IBox getParent() {
+		return parent;
+	}
+
+	@Override
+	public int getAbsoluteTop() {
+		if (parent == null) {
+			return top;
+		}
+		return parent.getAbsoluteTop() + top;
+	}
+
+	@Override
+	public int getAbsoluteLeft() {
+		if (parent == null) {
+			return left;
+		}
+		return parent.getAbsoluteLeft() + left;
+	}
+
+	@Override
+	public int getTop() {
+		return top;
+	}
+
+	@Override
+	public int getLeft() {
+		return left;
+	}
+
+	@Override
+	public void setPosition(final int top, final int left) {
+		this.top = top;
+		this.left = left;
+	}
+
+	@Override
+	public int getWidth() {
+		return width;
+	}
+
+	@Override
+	public int getHeight() {
+		return height;
+	}
+
+	@Override
+	public int getBaseline() {
+		if (component == null) {
+			return 0;
+		}
+		return component.getTop() + component.getBaseline();
+	}
+
+	@Override
+	public int getMaxWidth() {
+		return maxWidth;
+	}
+
+	@Override
+	public void setMaxWidth(final int maxWidth) {
+		this.maxWidth = maxWidth;
+	}
+
+	@Override
+	public int getInvisibleGapAtStart(final Graphics graphics) {
+		if (component == null) {
+			return 0;
+		}
+		return component.getInvisibleGapAtStart(graphics);
+	}
+
+	@Override
+	public int getInvisibleGapAtEnd(final Graphics graphics) {
+		if (component == null) {
+			return 0;
+		}
+		return component.getInvisibleGapAtEnd(graphics);
+	}
+
+	@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);
+	}
+
+	@Override
+	public void accept(final IBoxVisitor visitor) {
+		visitor.visit(this);
+	}
+
+	@Override
+	public <T> T accept(final IBoxVisitorWithResult<T> visitor) {
+		return visitor.visit(this);
+	}
+
+	public Margin getMargin() {
+		return margin;
+	}
+
+	public void setMargin(final Margin margin) {
+		this.margin = margin;
+	}
+
+	public Border getBorder() {
+		return border;
+	}
+
+	public void setBorder(final Border border) {
+		this.border = border;
+	}
+
+	public Padding getPadding() {
+		return padding;
+	}
+
+	public void setPadding(final Padding padding) {
+		this.padding = padding;
+	}
+
+	public Color getBackgroundColor() {
+		return backgroundColor;
+	}
+
+	public void setBackgroundColor(final Color backgroundColor) {
+		this.backgroundColor = backgroundColor;
+	}
+
+	@Override
+	public void setComponent(final IInlineBox component) {
+		this.component = component;
+		component.setParent(this);
+	}
+
+	@Override
+	public IInlineBox getComponent() {
+		return component;
+	}
+
+	@Override
+	public void layout(final Graphics graphics) {
+		if (component == null) {
+			width = 0;
+			height = 0;
+			return;
+		}
+
+		layoutComponent(graphics);
+		calculateBounds();
+	}
+
+	private void calculateBounds() {
+		if (component == null || component.getWidth() == 0 || component.getHeight() == 0) {
+			height = 0;
+			width = 0;
+		} else {
+			height = topFrame(component.getHeight()) + component.getHeight() + bottomFrame(component.getHeight());
+			width = leftFrame(component.getWidth()) + component.getWidth() + rightFrame(component.getWidth());
+		}
+	}
+
+	private void layoutComponent(final Graphics graphics) {
+		component.setMaxWidth(maxWidth);
+		component.layout(graphics);
+		component.setPosition(topFrame(component.getHeight()), leftFrame(component.getWidth()));
+	}
+
+	@Override
+	public boolean reconcileLayout(final Graphics graphics) {
+		final int oldHeight = height;
+		final int oldWidth = width;
+
+		calculateBounds();
+
+		return oldHeight != height || oldWidth != width;
+	}
+
+	private int topFrame(final int componentHeight) {
+		return margin.top.get(componentHeight) + border.top.width + padding.top.get(componentHeight);
+	}
+
+	private int leftFrame(final int componentWidth) {
+		return margin.left.get(componentWidth) + border.left.width + padding.left.get(componentWidth);
+	}
+
+	private int bottomFrame(final int componentHeight) {
+		return margin.bottom.get(componentHeight) + border.bottom.width + padding.bottom.get(componentHeight);
+	}
+
+	private int rightFrame(final int componentWidth) {
+		return margin.right.get(componentWidth) + border.right.width + padding.right.get(componentWidth);
+	}
+
+	@Override
+	public void paint(final Graphics graphics) {
+		drawBackground(graphics);
+		drawBorder(graphics);
+		paintComponent(graphics);
+	}
+
+	private void drawBackground(final Graphics graphics) {
+		if (backgroundColor == null) {
+			return;
+		}
+
+		graphics.setBackground(graphics.getColor(backgroundColor));
+		graphics.fillRect(0, 0, width, height);
+	}
+
+	private void drawBorder(final Graphics graphics) {
+		final int rectTop = margin.top.get(component.getHeight()) + border.top.width / 2;
+		final int rectLeft = margin.left.get(component.getWidth()) + border.left.width / 2;
+		final int rectBottom = height - margin.bottom.get(component.getHeight()) - border.bottom.width / 2;
+		final int rectRight = width - margin.right.get(component.getWidth()) - border.right.width / 2;
+
+		drawBorderLine(graphics, border.top, rectTop, rectLeft - border.left.width / 2, rectTop, rectRight + border.right.width / 2);
+		drawBorderLine(graphics, border.left, rectTop - border.top.width / 2, rectLeft, rectBottom + border.bottom.width / 2, rectLeft);
+		drawBorderLine(graphics, border.bottom, rectBottom, rectLeft - border.left.width / 2, rectBottom, rectRight + border.right.width / 2);
+		drawBorderLine(graphics, border.right, rectTop - border.top.width / 2, rectRight, rectBottom + border.bottom.width / 2, rectRight);
+	}
+
+	private void drawBorderLine(final Graphics graphics, final BorderLine borderLine, final int top, final int left, final int bottom, final int right) {
+		if (borderLine.width <= 0) {
+			return;
+		}
+		graphics.setLineStyle(borderLine.style);
+		graphics.setLineWidth(borderLine.width);
+		graphics.setColor(graphics.getColor(borderLine.color));
+		graphics.drawLine(left, top, right, bottom);
+	}
+
+	private void paintComponent(final Graphics graphics) {
+		ChildBoxPainter.paint(component, graphics);
+	}
+
+	@Override
+	public boolean canJoin(final IInlineBox other) {
+		if (!(other instanceof InlineFrame)) {
+			return false;
+		}
+		final InlineFrame otherFrame = (InlineFrame) other;
+
+		if (!margin.equals(otherFrame.margin) || !border.equals(otherFrame.border) || !padding.equals(otherFrame.padding)) {
+			return false;
+		}
+		if (backgroundColor == null && otherFrame.backgroundColor != null || backgroundColor != null && !backgroundColor.equals(otherFrame.backgroundColor)) {
+			return false;
+		}
+		if (!component.canJoin(otherFrame.component)) {
+			return false;
+		}
+
+		return true;
+	}
+
+	@Override
+	public boolean join(final IInlineBox other) {
+		if (!canJoin(other)) {
+			return false;
+		}
+		final InlineFrame otherFrame = (InlineFrame) other;
+
+		component.join(otherFrame.component);
+
+		calculateBounds();
+
+		return true;
+	}
+
+	@Override
+	public boolean canSplit() {
+		if (component == null) {
+			return false;
+		}
+		return component.canSplit();
+	}
+
+	@Override
+	public IInlineBox splitTail(final Graphics graphics, final int headWidth, final boolean force) {
+		final IInlineBox tailComponent;
+		final int tailHeadWidth = headWidth - leftFrame(component.getWidth());
+		if (tailHeadWidth < 0) {
+			tailComponent = component;
+			component = null;
+		} else {
+			tailComponent = component.splitTail(graphics, tailHeadWidth, force);
+		}
+
+		final InlineFrame tail = new InlineFrame();
+		tail.setComponent(tailComponent);
+		tail.setParent(parent);
+		tail.setMargin(margin);
+		tail.setBorder(border);
+		tail.setPadding(padding);
+		tail.setBackgroundColor(backgroundColor);
+		tail.layout(graphics);
+
+		layout(graphics);
+
+		return tail;
+	}
+
+	@Override
+	public String toString() {
+		return "InlineFrame [top=" + top + ", left=" + left + ", width=" + width + ", height=" + height + ", margin=" + margin + ", border=" + border + ", padding=" + padding + "]";
+	}
+
+}
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
new file mode 100644
index 0000000..5fae4e3
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/InlineNodeReference.java
@@ -0,0 +1,481 @@
+/*******************************************************************************
+ * 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;
+
+import java.text.MessageFormat;
+
+import org.eclipse.core.runtime.Assert;
+import org.eclipse.vex.core.internal.core.Color;
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+import org.eclipse.vex.core.provisional.dom.ContentRange;
+import org.eclipse.vex.core.provisional.dom.IContent;
+import org.eclipse.vex.core.provisional.dom.INode;
+import org.eclipse.vex.core.provisional.dom.IPosition;
+
+/**
+ * @author Florian Thienel
+ */
+public class InlineNodeReference extends BaseBox implements IInlineBox, IDecoratorBox<IInlineBox>, IContentBox {
+
+	private IBox parent;
+	private int top;
+	private int left;
+	private int width;
+	private int height;
+	private int baseline;
+	private int maxWidth;
+
+	private IInlineBox component;
+
+	private INode node;
+	private IPosition startPosition;
+	private IPosition endPosition;
+	private boolean canContainText;
+
+	@Override
+	public void setParent(final IBox parent) {
+		this.parent = parent;
+	}
+
+	@Override
+	public IBox getParent() {
+		return parent;
+	}
+
+	@Override
+	public int getAbsoluteTop() {
+		if (parent == null) {
+			return top;
+		}
+		return parent.getAbsoluteTop() + top;
+	}
+
+	@Override
+	public int getAbsoluteLeft() {
+		if (parent == null) {
+			return left;
+		}
+		return parent.getAbsoluteLeft() + left;
+	}
+
+	@Override
+	public int getTop() {
+		return top;
+	}
+
+	@Override
+	public int getLeft() {
+		return left;
+	}
+
+	@Override
+	public void setPosition(final int top, final int left) {
+		this.top = top;
+		this.left = left;
+	}
+
+	@Override
+	public int getWidth() {
+		return width;
+	}
+
+	@Override
+	public int getHeight() {
+		return height;
+	}
+
+	@Override
+	public int getBaseline() {
+		return baseline;
+	}
+
+	@Override
+	public int getMaxWidth() {
+		return maxWidth;
+	}
+
+	@Override
+	public void setMaxWidth(final int maxWidth) {
+		this.maxWidth = maxWidth;
+	}
+
+	@Override
+	public int getInvisibleGapAtStart(final Graphics graphics) {
+		if (component == null) {
+			return 0;
+		}
+		return component.getInvisibleGapAtStart(graphics);
+	}
+
+	@Override
+	public int getInvisibleGapAtEnd(final Graphics graphics) {
+		if (component == null) {
+			return 0;
+		}
+		return component.getInvisibleGapAtEnd(graphics);
+	}
+
+	@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);
+	}
+
+	@Override
+	public void accept(final IBoxVisitor visitor) {
+		visitor.visit(this);
+	}
+
+	@Override
+	public <T> T accept(final IBoxVisitorWithResult<T> visitor) {
+		return visitor.visit(this);
+	}
+
+	@Override
+	public void setComponent(final IInlineBox component) {
+		this.component = component;
+		component.setParent(this);
+	}
+
+	@Override
+	public IInlineBox getComponent() {
+		return component;
+	}
+
+	public void setNode(final INode node) {
+		setSubrange(node, node.getStartOffset(), node.getEndOffset());
+	}
+
+	private void setSubrange(final INode node, final int startOffset, final int endOffset) {
+		this.node = node;
+		startPosition = node.getContent().createPosition(startOffset);
+		endPosition = node.getContent().createPosition(endOffset);
+	}
+
+	public INode getNode() {
+		return node;
+	}
+
+	public void setCanContainText(final boolean canContainText) {
+		this.canContainText = canContainText;
+	}
+
+	public boolean canContainText() {
+		return canContainText;
+	}
+
+	@Override
+	public void layout(final Graphics graphics) {
+		if (component == null) {
+			return;
+		}
+		component.setPosition(0, 0);
+		component.setMaxWidth(maxWidth);
+		component.layout(graphics);
+		width = component.getWidth();
+		height = component.getHeight();
+		baseline = component.getBaseline();
+	}
+
+	@Override
+	public boolean reconcileLayout(final Graphics graphics) {
+		final int oldWidth = width;
+		final int oldHeight = height;
+		final int oldBaseline = baseline;
+		width = component.getWidth();
+		height = component.getHeight();
+		baseline = component.getBaseline();
+
+		layout(graphics);
+
+		return oldWidth != width || oldHeight != height || oldBaseline != baseline;
+	}
+
+	@Override
+	public void paint(final Graphics graphics) {
+		ChildBoxPainter.paint(component, graphics);
+	}
+
+	@Override
+	public void highlight(final Graphics graphics, final Color foreground, final Color background) {
+		fillBackground(graphics, background);
+
+		accept(new DepthFirstBoxTraversal<Object>() {
+			@Override
+			public Object visit(final InlineNodeReference box) {
+				if (box != InlineNodeReference.this) {
+					box.highlightInside(graphics, foreground, background);
+				}
+				return super.visit(box);
+			}
+
+			@Override
+			public Object visit(final TextContent box) {
+				box.highlight(graphics, foreground, background);
+				return super.visit(box);
+			}
+
+			@Override
+			public Object visit(final NodeEndOffsetPlaceholder box) {
+				box.highlight(graphics, foreground, background);
+				return super.visit(box);
+			}
+
+			@Override
+			public Object visit(final NodeTag box) {
+				paintBox(graphics, box);
+				return super.visit(box);
+			}
+
+			@Override
+			public Object visit(final GraphicalBullet box) {
+				paintBox(graphics, box);
+				return super.visit(box);
+			}
+
+			@Override
+			public Object visit(final Square box) {
+				paintBox(graphics, box);
+				return super.visit(box);
+			}
+
+			@Override
+			public Object visit(final StaticText box) {
+				paintBox(graphics, box);
+				return super.visit(box);
+			}
+
+			@Override
+			public Object visit(final Image box) {
+				paintBox(graphics, box);
+				return super.visit(box);
+			}
+
+			private void paintBox(final Graphics graphics, final IBox box) {
+				graphics.moveOrigin(box.getAbsoluteLeft(), box.getAbsoluteTop());
+				box.paint(graphics);
+				graphics.moveOrigin(-box.getAbsoluteLeft(), -box.getAbsoluteTop());
+			}
+		});
+	}
+
+	private void fillBackground(final Graphics graphics, final Color color) {
+		graphics.setForeground(graphics.getColor(color));
+		graphics.setBackground(graphics.getColor(color));
+		graphics.fillRect(getAbsoluteLeft(), getAbsoluteTop(), width, height);
+	}
+
+	public void highlightInside(final Graphics graphics, final Color foreground, final Color background) {
+		fillBackground(graphics, background);
+	}
+
+	@Override
+	public IContent getContent() {
+		return node.getContent();
+	}
+
+	@Override
+	public int getStartOffset() {
+		return startPosition.getOffset();
+	}
+
+	@Override
+	public int getEndOffset() {
+		return endPosition.getOffset();
+	}
+
+	@Override
+	public ContentRange getRange() {
+		if (node == null) {
+			return ContentRange.NULL;
+		}
+		return new ContentRange(getStartOffset(), getEndOffset());
+	}
+
+	@Override
+	public Rectangle getPositionArea(final Graphics graphics, final int offset) {
+		return new Rectangle(0, 0, width, height);
+	}
+
+	@Override
+	public int getOffsetForCoordinates(final Graphics graphics, final int x, final int y) {
+		if (isEmpty()) {
+			return getEndOffset();
+		}
+		final int half = width / 2;
+		if (x < half) {
+			return getStartOffset();
+		} else {
+			return getEndOffset();
+		}
+	}
+
+	@Override
+	public boolean isEmpty() {
+		return getEndOffset() - getStartOffset() <= 1;
+	}
+
+	@Override
+	public boolean isAtStart(final int offset) {
+		return getStartOffset() == offset;
+	}
+
+	@Override
+	public boolean isAtEnd(final int offset) {
+		return getEndOffset() == offset;
+	}
+
+	@Override
+	public boolean canJoin(final IInlineBox other) {
+		if (!(other instanceof InlineNodeReference)) {
+			return false;
+		}
+		final InlineNodeReference otherNodeReference = (InlineNodeReference) other;
+		if (node != otherNodeReference.node) {
+			return false;
+		}
+		if (endPosition.getOffset() != otherNodeReference.getStartOffset() - 1 && endPosition.getOffset() != otherNodeReference.getStartOffset()) {
+			return false;
+		}
+		if (!component.canJoin(otherNodeReference.component)) {
+			return false;
+		}
+		return true;
+	}
+
+	@Override
+	public boolean join(final IInlineBox other) {
+		if (!canJoin(other)) {
+			return false;
+		}
+		final InlineNodeReference otherNodeReference = (InlineNodeReference) other;
+
+		component.join(otherNodeReference.component);
+
+		node.getContent().removePosition(endPosition);
+		node.getContent().removePosition(otherNodeReference.startPosition);
+		endPosition = otherNodeReference.endPosition;
+
+		width = component.getWidth();
+		height = component.getHeight();
+		baseline = component.getBaseline();
+
+		return true;
+	}
+
+	@Override
+	public boolean canSplit() {
+		if (component == null) {
+			return false;
+		}
+		return component.canSplit();
+	}
+
+	@Override
+	public IInlineBox splitTail(final Graphics graphics, final int headWidth, final boolean force) {
+		final int firstChildOffset = findStartOffset(component);
+
+		final IInlineBox tailComponent = component.splitTail(graphics, headWidth, force);
+
+		final InlineNodeReference tail = new InlineNodeReference();
+		tail.setComponent(tailComponent);
+		tail.setCanContainText(canContainText);
+		tail.setParent(parent);
+		tail.layout(graphics);
+
+		layout(graphics);
+
+		adaptContentRanges(firstChildOffset, tail);
+
+		return tail;
+	}
+
+	private void adaptContentRanges(final int oldOffsetOfFirstChild, final InlineNodeReference tail) {
+		if (tail.getWidth() == 0) {
+			return;
+		}
+
+		final int offsetOfFirstChildInTail = findStartOffset(tail.getComponent());
+		int splitPosition;
+		if (offsetOfFirstChildInTail == -1) {
+			splitPosition = endPosition.getOffset();
+		} else if (offsetOfFirstChildInTail == oldOffsetOfFirstChild && (width == 0 || offsetOfFirstChildInTail > endPosition.getOffset())) {
+			splitPosition = startPosition.getOffset();
+		} else {
+			splitPosition = offsetOfFirstChildInTail;
+		}
+
+		Assert.isTrue(splitPosition >= getStartOffset(), MessageFormat.format("Split position {0} is invalid.", splitPosition));
+
+		tail.setSubrange(node, splitPosition, getEndOffset());
+		node.getContent().removePosition(endPosition);
+		endPosition = node.getContent().createPosition(Math.max(startPosition.getOffset(), splitPosition - 1));
+
+		Assert.isTrue(startPosition.getOffset() <= endPosition.getOffset(), "InlineNodeReference head range is invalid: [" + startPosition + ", " + endPosition + "]");
+		Assert.isTrue(tail.startPosition.getOffset() <= tail.endPosition.getOffset(), "InlineNodeReference tail range is invalid: [" + tail.startPosition + ", " + tail.endPosition + "]");
+	}
+
+	private int findStartOffset(final IBox startBox) {
+		final Integer startOffset = startBox.accept(new DepthFirstBoxTraversal<Integer>() {
+			@Override
+			public Integer visit(final InlineNodeReference box) {
+				if (box == startBox) {
+					return super.visit(box);
+				}
+				return box.getStartOffset();
+			}
+
+			@Override
+			public Integer visit(final TextContent box) {
+				return box.getStartOffset();
+			}
+
+			@Override
+			public Integer visit(final NodeEndOffsetPlaceholder box) {
+				return box.getStartOffset();
+			}
+		});
+		if (startOffset == null) {
+			return -1;
+		}
+		return startOffset;
+	}
+
+	@Override
+	public String toString() {
+		return "InlineNodeReference [top=" + top + ", left=" + left + ", width=" + width + ", height=" + height + ", baseline=" + baseline + ", startPosition=" + startPosition
+				+ ", endPosition=" + endPosition + ", canContainText=" + canContainText + "]";
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/Line.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/Line.java
new file mode 100644
index 0000000..25c4f04
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/Line.java
@@ -0,0 +1,170 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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;
+
+import java.util.LinkedList;
+
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+
+/**
+ * @author Florian Thienel
+ */
+public class Line {
+
+	private int top;
+	private int left;
+	private int width;
+	private int height;
+	private int baseline;
+
+	private final LinkedList<IInlineBox> children = new LinkedList<IInlineBox>();
+
+	public void setPosition(final int top, final int left) {
+		translateChildrenToNewPosition(top, left);
+		this.top = top;
+		this.left = left;
+	}
+
+	private void translateChildrenToNewPosition(final int top, final int left) {
+		for (final IInlineBox child : children) {
+			child.setPosition(child.getTop() - this.top + top, child.getLeft() - this.left + left);
+		}
+	}
+
+	public Rectangle getBounds() {
+		return new Rectangle(left, top, width, height);
+	}
+
+	public int getTop() {
+		return top;
+	}
+
+	public int getLeft() {
+		return left;
+	}
+
+	public int getWidth() {
+		return width;
+	}
+
+	public int getHeight() {
+		return height;
+	}
+
+	public int getBaseline() {
+		return baseline;
+	}
+
+	public int getInvisibleGapLeft(final Graphics graphics) {
+		if (children.isEmpty()) {
+			return 0;
+		}
+		return children.getFirst().getInvisibleGapAtStart(graphics);
+	}
+
+	public int getInvisibleGapRight(final Graphics graphics) {
+		if (children.isEmpty()) {
+			return 0;
+		}
+		return children.getLast().getInvisibleGapAtEnd(graphics);
+	}
+
+	public boolean hasChildren() {
+		return !children.isEmpty();
+	}
+
+	public boolean hasMoreThanOneChild() {
+		return !children.isEmpty() && children.getFirst() != children.getLast();
+	}
+
+	public void prependChild(final IInlineBox box) {
+		children.addFirst(box);
+		width += box.getWidth();
+	}
+
+	public void appendChild(final IInlineBox box) {
+		children.addLast(box);
+		width += box.getWidth();
+	}
+
+	public boolean canJoinWithLastChild(final IInlineBox box) {
+		if (children.isEmpty()) {
+			return false;
+		}
+		final IInlineBox lastChild = children.getLast();
+		return lastChild.canJoin(box);
+	}
+
+	public boolean joinWithLastChild(final IInlineBox box) {
+		if (children.isEmpty()) {
+			return false;
+		}
+		final IInlineBox lastChild = children.getLast();
+		final int lastChildOldWidth = lastChild.getWidth();
+		final boolean joined = lastChild.join(box);
+		if (joined) {
+			width += lastChild.getWidth() - lastChildOldWidth;
+		}
+		return joined;
+	}
+
+	public IInlineBox getLastChild() {
+		return children.getLast();
+	}
+
+	public void removeLastChild() {
+		if (children.isEmpty()) {
+			return;
+		}
+		children.removeLast();
+	}
+
+	public void shiftBy(final int xOffset) {
+		if (xOffset == 0) {
+			return;
+		}
+		for (final IInlineBox child : children) {
+			child.setPosition(child.getTop(), child.getLeft() + xOffset);
+		}
+	}
+
+	public void arrangeChildren() {
+		calculateBoundsAndBaseline();
+		arrangeChildrenOnBaseline();
+	}
+
+	private void calculateBoundsAndBaseline() {
+		width = 0;
+		height = 0;
+		baseline = 0;
+		int descend = 0;
+		for (final IInlineBox child : children) {
+			width += child.getWidth();
+			descend = Math.max(descend, child.getHeight() - child.getBaseline());
+			baseline = Math.max(baseline, child.getBaseline());
+		}
+		height = baseline + descend;
+	}
+
+	private void arrangeChildrenOnBaseline() {
+		int childLeft = left;
+		for (final IInlineBox child : children) {
+			final int childTop = baseline - child.getBaseline() + top;
+			child.setPosition(childTop, childLeft);
+			childLeft += child.getWidth();
+		}
+	}
+
+	public void paint(final Graphics graphics) {
+		ChildBoxPainter.paint(children, graphics);
+	}
+}
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
new file mode 100644
index 0000000..6529bf9
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/LineArrangement.java
@@ -0,0 +1,212 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.ListIterator;
+
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.TextAlign;
+
+public class LineArrangement {
+
+	private final LinkedList<Line> lines = new LinkedList<Line>();
+
+	private ListIterator<IInlineBox> boxIterator;
+	private int width;
+	private int height;
+	private boolean lastBoxWrappedCompletely;
+	private Line currentLine;
+
+	public void arrangeBoxes(final Graphics graphics, final ListIterator<IInlineBox> boxIterator, final int width, final TextAlign textAlign) {
+		this.boxIterator = boxIterator;
+		this.width = width;
+		reset();
+
+		while (boxIterator.hasNext()) {
+			final IInlineBox box = boxIterator.next();
+			box.setMaxWidth(width);
+			box.layout(graphics);
+			appendBox(graphics, box);
+		}
+		finalizeCurrentLine();
+
+		alignLines(graphics, textAlign);
+	}
+
+	private void reset() {
+		lines.clear();
+		height = 0;
+		lastBoxWrappedCompletely = false;
+		currentLine = new Line();
+	}
+
+	private void appendBox(final Graphics graphics, final IInlineBox box) {
+		final boolean boxWrappedCompletely;
+		if (currentLine.canJoinWithLastChild(box)) {
+			boxWrappedCompletely = arrangeWithLastChild(graphics, box);
+		} else if ((boxFitsIntoCurrentLine(box) || !hasVisibleContent(box)) && !box.requiresSplitForLineWrapping()) {
+			boxWrappedCompletely = appendToCurrentLine(box);
+		} else if (box.canSplit()) {
+			boxWrappedCompletely = splitAndWrapToNextLine(graphics, box);
+		} else {
+			boxWrappedCompletely = wrapCompletelyToNextLine(box);
+		}
+
+		lastBoxWrappedCompletely = boxWrappedCompletely;
+	}
+
+	private static boolean hasVisibleContent(final IInlineBox box) {
+		final Boolean result = box.accept(new DepthFirstBoxTraversal<Boolean>() {
+			@Override
+			public Boolean visit(final TextContent box) {
+				return box.getText().trim().length() > 0;
+			}
+
+			@Override
+			public Boolean visit(final StaticText box) {
+				return box.getText().trim().length() > 0;
+			}
+		});
+		if (result == null) {
+			return true;
+		}
+		return result;
+	}
+
+	private boolean arrangeWithLastChild(final Graphics graphics, final IInlineBox box) {
+		currentLine.joinWithLastChild(box);
+		boxIterator.remove();
+		if (currentLine.getWidth() <= width) {
+			return false;
+		}
+
+		final IInlineBox lastChild = currentLine.getLastChild();
+		if (!lastChild.canSplit()) {
+			throw new IllegalStateException("An IInlineBox that supports joining must also support splitting!");
+		}
+
+		final int headWidth = lastChild.getWidth() - currentLine.getWidth() + width;
+		final IInlineBox tail = lastChild.splitTail(graphics, headWidth, !currentLine.hasMoreThanOneChild());
+		final boolean lastChildWrappedCompletely = lastChild.getWidth() == 0;
+		if (lastChildWrappedCompletely) {
+			removeLastChild();
+		}
+		if (tail.getWidth() > 0) {
+			insertNextBox(tail);
+			lineBreak();
+		}
+
+		return lastChildWrappedCompletely;
+	}
+
+	private void removeLastChild() {
+		boxIterator.previous();
+		boxIterator.remove();
+		currentLine.removeLastChild();
+	}
+
+	private boolean appendToCurrentLine(final IInlineBox box) {
+		currentLine.appendChild(box);
+		return false;
+	}
+
+	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;
+		if (boxWrappedCompletely) {
+			boxIterator.remove();
+		} else {
+			currentLine.appendChild(box);
+		}
+
+		if (tail.getWidth() > 0) {
+			insertNextBox(tail);
+			lineBreak();
+		} else if (box.getLineWrappingAtEnd() == LineWrappingRule.REQUIRED) {
+			lineBreak();
+		}
+
+		return boxWrappedCompletely;
+	}
+
+	private boolean wrapCompletelyToNextLine(final IInlineBox box) {
+		lineBreak();
+		if (boxFitsIntoCurrentLine(box)) {
+			currentLine.appendChild(box);
+		}
+		return true;
+	}
+
+	private void insertNextBox(final IInlineBox box) {
+		boxIterator.add(box);
+		backupBoxIterator();
+	}
+
+	private void backupBoxIterator() {
+		if (!lastBoxWrappedCompletely) {
+			boxIterator.previous();
+		}
+	}
+
+	private boolean boxFitsIntoCurrentLine(final IInlineBox box) {
+		return currentLine.getWidth() + box.getWidth() <= width;
+	}
+
+	private void lineBreak() {
+		finalizeCurrentLine();
+		currentLine = new Line();
+	}
+
+	private void finalizeCurrentLine() {
+		if (currentLine.getWidth() <= 0) {
+			return;
+		}
+		currentLine.arrangeChildren();
+		currentLine.setPosition(height, 0);
+		height += currentLine.getHeight();
+		lines.add(currentLine);
+	}
+
+	private void alignLines(final Graphics graphics, final TextAlign textAlign) {
+		if (textAlign == TextAlign.LEFT) {
+			return;
+		}
+		for (final Line line : lines) {
+			line.shiftBy(alignmentOffset(graphics, line, textAlign));
+		}
+	}
+
+	private int alignmentOffset(final Graphics graphics, final Line line, final TextAlign textAlign) {
+		switch (textAlign) {
+		case CENTER:
+			return (width - line.getWidth() + line.getInvisibleGapLeft(graphics) + line.getInvisibleGapRight(graphics)) / 2 - line.getInvisibleGapLeft(graphics);
+		case RIGHT:
+			return width - line.getWidth() + line.getInvisibleGapRight(graphics);
+		default:
+			return 0;
+		}
+	}
+
+	public Collection<Line> getLines() {
+		return lines;
+	}
+
+	public int getHeight() {
+		return height;
+	}
+}
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/List.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/List.java
new file mode 100644
index 0000000..b09a4fe
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/List.java
@@ -0,0 +1,191 @@
+package org.eclipse.vex.core.internal.boxes;
+
+import java.util.ArrayList;
+
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+import org.eclipse.vex.core.internal.css.BulletStyle;
+
+public class List extends BaseBox implements IStructuralBox, IDecoratorBox<IStructuralBox> {
+
+	private IBox parent;
+	private int top;
+	private int left;
+	private int width;
+	private int height;
+
+	private BulletStyle bulletStyle;
+	private IBulletFactory bulletFactory;
+
+	private IStructuralBox component;
+
+	@Override
+	public void setParent(final IBox parent) {
+		this.parent = parent;
+	}
+
+	@Override
+	public IBox getParent() {
+		return parent;
+	}
+
+	@Override
+	public int getAbsoluteTop() {
+		if (parent == null) {
+			return top;
+		}
+		return parent.getAbsoluteTop() + top;
+	}
+
+	@Override
+	public int getAbsoluteLeft() {
+		if (parent == null) {
+			return left;
+		}
+		return parent.getAbsoluteLeft() + left;
+	}
+
+	public int getTop() {
+		return top;
+	}
+
+	public int getLeft() {
+		return left;
+	}
+
+	public void setPosition(final int top, final int left) {
+		this.top = top;
+		this.left = left;
+	}
+
+	@Override
+	public int getWidth() {
+		return width;
+	}
+
+	public void setWidth(final int width) {
+		this.width = Math.max(0, width);
+	}
+
+	@Override
+	public int getHeight() {
+		return height;
+	}
+
+	@Override
+	public Rectangle getBounds() {
+		return new Rectangle(left, top, width, height);
+	}
+
+	@Override
+	public void accept(final IBoxVisitor visitor) {
+		visitor.visit(this);
+	}
+
+	@Override
+	public <T> T accept(final IBoxVisitorWithResult<T> visitor) {
+		return visitor.visit(this);
+	}
+
+	public void setBulletStyle(final BulletStyle bulletStyle) {
+		this.bulletStyle = bulletStyle;
+	}
+
+	public BulletStyle getBulletStyle() {
+		return bulletStyle;
+	}
+
+	public void setBulletFactory(final IBulletFactory bulletFactory) {
+		this.bulletFactory = bulletFactory;
+	}
+
+	public IBulletFactory getBulletFactory() {
+		return bulletFactory;
+	}
+
+	@Override
+	public void setComponent(final IStructuralBox component) {
+		this.component = component;
+		component.setParent(this);
+	}
+
+	@Override
+	public IStructuralBox getComponent() {
+		return component;
+	}
+
+	@Override
+	public void layout(final Graphics graphics) {
+		if (component == null) {
+			return;
+		}
+
+		layoutBullets(graphics, collectListItems(this));
+		layoutComponent(graphics);
+
+		height = component.getHeight();
+	}
+
+	private void layoutBullets(final Graphics graphics, final java.util.List<ListItem> listItems) {
+		int bulletWidth = 0;
+		for (int i = 0; i < listItems.size(); i += 1) {
+			final IInlineBox bullet;
+			if (bulletFactory == null) {
+				bullet = null;
+			} else {
+				bullet = bulletFactory.createBullet(bulletStyle, i, listItems.size());
+				bullet.layout(graphics);
+				bulletWidth = Math.max(bulletWidth, bullet.getWidth());
+			}
+
+			listItems.get(i).setBullet(bullet);
+		}
+
+		for (final ListItem listItem : listItems) {
+			listItem.setBulletPosition(bulletStyle.position);
+			listItem.setBulletWidth(bulletWidth);
+		}
+	}
+
+	private static java.util.List<ListItem> collectListItems(final List list) {
+		final ArrayList<ListItem> listItems = new ArrayList<ListItem>();
+		list.accept(new DepthFirstBoxTraversal<Object>() {
+			@Override
+			public Object visit(final List box) {
+				if (box == list) {
+					return super.visit(box);
+				}
+				return null;
+			}
+
+			@Override
+			public Object visit(final ListItem box) {
+				listItems.add(box);
+				return null;
+			}
+		});
+		return listItems;
+	}
+
+	private void layoutComponent(final Graphics graphics) {
+		component.setPosition(0, 0);
+		component.setWidth(width);
+		component.layout(graphics);
+	}
+
+	@Override
+	public boolean reconcileLayout(final Graphics graphics) {
+		final int oldHeight = height;
+		height = component.getHeight();
+		if (oldHeight != height) {
+			layout(graphics);
+			return true;
+		}
+		return false;
+	}
+
+	@Override
+	public void paint(final Graphics graphics) {
+		ChildBoxPainter.paint(component, graphics);
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/ListItem.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/ListItem.java
new file mode 100644
index 0000000..6717e4f
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/ListItem.java
@@ -0,0 +1,368 @@
+/*******************************************************************************
+ * Copyright (c) 2016 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;
+
+import static org.eclipse.vex.core.internal.boxes.BoxFactory.frame;
+
+import java.util.Iterator;
+
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+import org.eclipse.vex.core.internal.core.TextAlign;
+import org.eclipse.vex.core.internal.css.BulletStyle;
+
+public class ListItem extends BaseBox implements IStructuralBox, IDecoratorBox<IStructuralBox> {
+
+	public static final int BULLET_SPACING = 5;
+
+	private IBox parent;
+	private int top;
+	private int left;
+	private int width;
+	private int height;
+
+	private IInlineBox bullet;
+
+	private BulletStyle.Position bulletPosition = BulletStyle.Position.OUTSIDE;
+	private int bulletWidth;
+	private TextAlign bulletAlign = TextAlign.RIGHT;
+
+	private Paragraph bulletContainer;
+	private IStructuralBox component;
+
+	@Override
+	public void setParent(final IBox parent) {
+		this.parent = parent;
+	}
+
+	@Override
+	public IBox getParent() {
+		return parent;
+	}
+
+	@Override
+	public int getAbsoluteTop() {
+		if (parent == null) {
+			return top;
+		}
+		return parent.getAbsoluteTop() + top;
+	}
+
+	@Override
+	public int getAbsoluteLeft() {
+		if (parent == null) {
+			return left;
+		}
+		return parent.getAbsoluteLeft() + left;
+	}
+
+	public int getTop() {
+		return top;
+	}
+
+	public int getLeft() {
+		return left;
+	}
+
+	public void setPosition(final int top, final int left) {
+		this.top = top;
+		this.left = left;
+	}
+
+	@Override
+	public int getWidth() {
+		return width;
+	}
+
+	public void setWidth(final int width) {
+		this.width = Math.max(0, width);
+	}
+
+	@Override
+	public int getHeight() {
+		return height;
+	}
+
+	@Override
+	public Rectangle getBounds() {
+		return new Rectangle(left, top, width, height);
+	}
+
+	@Override
+	public void accept(final IBoxVisitor visitor) {
+		visitor.visit(this);
+	}
+
+	@Override
+	public <T> T accept(final IBoxVisitorWithResult<T> visitor) {
+		return visitor.visit(this);
+	}
+
+	public void setBulletPosition(final BulletStyle.Position bulletPosition) {
+		if (bulletPosition == this.bulletPosition) {
+			return;
+		}
+
+		this.bulletPosition = bulletPosition;
+		bulletContainer = null;
+	}
+
+	public BulletStyle.Position getBulletPosition() {
+		return bulletPosition;
+	}
+
+	public void setBulletWidth(final int bulletWidth) {
+		this.bulletWidth = bulletWidth;
+	}
+
+	public int getBulletWidth() {
+		return bulletWidth;
+	}
+
+	public void setBulletAlign(final TextAlign bulletAlign) {
+		this.bulletAlign = bulletAlign;
+	}
+
+	public TextAlign getBulletAlign() {
+		return bulletAlign;
+	}
+
+	public void setBullet(final IInlineBox bullet) {
+		this.bullet = bullet;
+		bulletContainer = null;
+	}
+
+	public IInlineBox getBullet() {
+		return bullet;
+	}
+
+	public void setComponent(final IStructuralBox component) {
+		this.component = component;
+		component.setParent(this);
+	}
+
+	@Override
+	public IStructuralBox getComponent() {
+		return component;
+	}
+
+	@Override
+	public void layout(final Graphics graphics) {
+		switch (bulletPosition) {
+		case OUTSIDE:
+			layoutWithOutsideBullet(graphics);
+			break;
+		case INSIDE:
+			layoutWithInsideBullet(graphics);
+			break;
+		default:
+			throw new AssertionError("Unknown BulletStyle.Position " + bulletPosition);
+		}
+
+		height = Math.max(getBulletHeight(), getComponentHeight());
+	}
+
+	private void layoutWithOutsideBullet(final Graphics graphics) {
+		if (bullet != null && bulletContainer == null) {
+			bulletContainer = new Paragraph();
+			bulletContainer.setParent(this);
+			bulletContainer.setTextAlign(bulletAlign);
+			bulletContainer.appendChild(bullet);
+		}
+
+		if (bulletContainer != null) {
+			bulletContainer.setWidth(bulletWidth);
+			bulletContainer.layout(graphics);
+		}
+
+		if (component != null) {
+			component.setWidth(getWidthForComponent());
+			component.layout(graphics);
+		}
+
+		final int bulletBaseline = findTopBaselineRelativeToParent(bulletContainer);
+		final int componentBaseline = findTopBaselineRelativeToParent(component);
+
+		final int baselineDelta = componentBaseline - bulletBaseline;
+		final int bulletTop;
+		final int componentTop;
+		if (baselineDelta > 0) {
+			bulletTop = baselineDelta;
+			componentTop = 0;
+		} else {
+			bulletTop = 0;
+			componentTop = -baselineDelta;
+		}
+
+		if (bulletContainer != null) {
+			bulletContainer.setPosition(bulletTop, 0);
+		}
+		if (component != null) {
+			component.setPosition(componentTop, width - component.getWidth());
+		}
+	}
+
+	private static int findTopBaselineRelativeToParent(final IStructuralBox parent) {
+		if (parent == null) {
+			return 0;
+		}
+
+		final Integer result = parent.accept(new DepthFirstBoxTraversal<Integer>() {
+			private int getBaselineRelativeToParent(final IInlineBox box) {
+				return box.getBaseline() + box.getAbsoluteTop() - parent.getAbsoluteTop();
+			}
+
+			@Override
+			public Integer visit(final InlineContainer box) {
+				return getBaselineRelativeToParent(box);
+			}
+
+			@Override
+			public Integer visit(final Image box) {
+				return getBaselineRelativeToParent(box);
+			}
+
+			@Override
+			public Integer visit(final InlineFrame box) {
+				return getBaselineRelativeToParent(box);
+			}
+
+			@Override
+			public Integer visit(final InlineNodeReference box) {
+				return getBaselineRelativeToParent(box);
+			}
+
+			@Override
+			public Integer visit(final NodeEndOffsetPlaceholder box) {
+				return getBaselineRelativeToParent(box);
+			}
+
+			@Override
+			public Integer visit(final NodeTag box) {
+				return getBaselineRelativeToParent(box);
+			}
+
+			@Override
+			public Integer visit(final GraphicalBullet box) {
+				return getBaselineRelativeToParent(box);
+			}
+
+			@Override
+			public Integer visit(final Square box) {
+				return getBaselineRelativeToParent(box);
+			}
+
+			@Override
+			public Integer visit(final StaticText box) {
+				return getBaselineRelativeToParent(box);
+			}
+
+			@Override
+			public Integer visit(final TextContent box) {
+				return getBaselineRelativeToParent(box);
+			}
+		});
+
+		if (result == null) {
+			return 0;
+		}
+		return result.intValue();
+	}
+
+	private void layoutWithInsideBullet(final Graphics graphics) {
+		if (component == null) {
+			return;
+		}
+
+		insertBulletIntoComponent();
+
+		component.setWidth(width);
+		component.setPosition(0, 0);
+		component.layout(graphics);
+	}
+
+	private void insertBulletIntoComponent() {
+		component.accept(new DepthFirstBoxTraversal<Object>() {
+			@Override
+			public Object visit(final Paragraph box) {
+				if (!isDecorated(box, bullet)) {
+					box.prependChild(frame(bullet, Margin.NULL, Border.NULL, new Padding(0, 0, 0, BULLET_SPACING), null));
+				}
+				return box;
+			}
+
+			private boolean isDecorated(final Paragraph box, final IInlineBox bullet) {
+				final IInlineBox firstChild = getFirstChild(box);
+				if (!(firstChild instanceof InlineFrame)) {
+					return false;
+				}
+				final InlineFrame frame = (InlineFrame) firstChild;
+
+				if (frame.getComponent().getClass().isAssignableFrom(bullet.getClass())) {
+					return true;
+				}
+
+				return false;
+			}
+
+			private IInlineBox getFirstChild(final Paragraph box) {
+				final Iterator<IInlineBox> iterator = box.getChildren().iterator();
+				if (iterator.hasNext()) {
+					return iterator.next();
+				}
+				return null;
+			}
+		});
+	}
+
+	@Override
+	public boolean reconcileLayout(final Graphics graphics) {
+		final int oldHeight = height;
+
+		if (bulletPosition == BulletStyle.Position.INSIDE && component != null) {
+			insertBulletIntoComponent();
+			component.layout(graphics);
+		}
+
+		height = Math.max(getBulletHeight(), getComponentHeight());
+
+		return oldHeight != height;
+	}
+
+	private int getBulletHeight() {
+		if (bulletContainer == null) {
+			return 0;
+		}
+		return bulletContainer.getHeight();
+	}
+
+	private int getComponentHeight() {
+		if (component == null) {
+			return 0;
+		}
+		return component.getHeight();
+	}
+
+	private int getWidthForComponent() {
+		if (bulletContainer == null) {
+			return width;
+		}
+		return width - bulletWidth - BULLET_SPACING;
+	}
+
+	@Override
+	public void paint(final Graphics graphics) {
+		if (bulletContainer != null) {
+			ChildBoxPainter.paint(bulletContainer, graphics);
+		}
+		ChildBoxPainter.paint(component, graphics);
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/Margin.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/Margin.java
new file mode 100644
index 0000000..42ad39c
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/Margin.java
@@ -0,0 +1,104 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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;
+
+import org.eclipse.vex.core.internal.core.Length;
+
+/**
+ * @author Florian Thienel
+ */
+public class Margin {
+
+	public static final Margin NULL = new Margin(0);
+
+	public final Length top;
+	public final Length left;
+	public final Length bottom;
+	public final Length right;
+
+	public Margin(final int size) {
+		this(size, size, size, size);
+	}
+
+	public Margin(final int vertical, final int horizontal) {
+		this(vertical, horizontal, vertical, horizontal);
+	}
+
+	public Margin(final int top, final int left, final int bottom, final int right) {
+		this(Length.absolute(top), Length.absolute(left), Length.absolute(bottom), Length.absolute(right));
+	}
+
+	public Margin(final Length top, final Length left, final Length bottom, final Length right) {
+		this.top = top;
+		this.left = left;
+		this.bottom = bottom;
+		this.right = right;
+	}
+
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = 1;
+		result = prime * result + (bottom == null ? 0 : bottom.hashCode());
+		result = prime * result + (left == null ? 0 : left.hashCode());
+		result = prime * result + (right == null ? 0 : right.hashCode());
+		result = prime * result + (top == null ? 0 : top.hashCode());
+		return result;
+	}
+
+	@Override
+	public boolean equals(final Object obj) {
+		if (this == obj) {
+			return true;
+		}
+		if (obj == null) {
+			return false;
+		}
+		if (getClass() != obj.getClass()) {
+			return false;
+		}
+		final Margin other = (Margin) obj;
+		if (bottom == null) {
+			if (other.bottom != null) {
+				return false;
+			}
+		} else if (!bottom.equals(other.bottom)) {
+			return false;
+		}
+		if (left == null) {
+			if (other.left != null) {
+				return false;
+			}
+		} else if (!left.equals(other.left)) {
+			return false;
+		}
+		if (right == null) {
+			if (other.right != null) {
+				return false;
+			}
+		} else if (!right.equals(other.right)) {
+			return false;
+		}
+		if (top == null) {
+			if (other.top != null) {
+				return false;
+			}
+		} else if (!top.equals(other.top)) {
+			return false;
+		}
+		return true;
+	}
+
+	@Override
+	public String toString() {
+		return "Margin [top=" + top + ", left=" + left + ", bottom=" + bottom + ", right=" + right + "]";
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/NodeEndOffsetPlaceholder.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/NodeEndOffsetPlaceholder.java
new file mode 100644
index 0000000..eba4066
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/NodeEndOffsetPlaceholder.java
@@ -0,0 +1,284 @@
+/*******************************************************************************
+ * 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;
+
+import org.eclipse.vex.core.internal.core.Color;
+import org.eclipse.vex.core.internal.core.FontMetrics;
+import org.eclipse.vex.core.internal.core.FontSpec;
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+import org.eclipse.vex.core.provisional.dom.ContentRange;
+import org.eclipse.vex.core.provisional.dom.IContent;
+import org.eclipse.vex.core.provisional.dom.INode;
+
+/**
+ * @author Florian Thienel
+ */
+public class NodeEndOffsetPlaceholder extends BaseBox implements IInlineBox, IContentBox {
+
+	private IBox parent;
+	private int top;
+	private int left;
+	private final int width = 1;
+	private int height;
+	private int baseline;
+	private int maxWidth;
+	private LineWrappingRule lineWrappingAtStart;
+	private LineWrappingRule lineWrappingAtEnd;
+
+	private INode node;
+
+	private FontSpec fontSpec;
+
+	private boolean layoutValid = false;
+
+	@Override
+	public void setParent(final IBox parent) {
+		this.parent = parent;
+	}
+
+	@Override
+	public IBox getParent() {
+		return parent;
+	}
+
+	@Override
+	public int getAbsoluteTop() {
+		if (parent == null) {
+			return top;
+		}
+		return parent.getAbsoluteTop() + top;
+	}
+
+	@Override
+	public int getAbsoluteLeft() {
+		if (parent == null) {
+			return left;
+		}
+		return parent.getAbsoluteLeft() + left;
+	}
+
+	@Override
+	public int getTop() {
+		return top;
+	}
+
+	@Override
+	public int getLeft() {
+		return left;
+	}
+
+	@Override
+	public void setPosition(final int top, final int left) {
+		this.top = top;
+		this.left = left;
+	}
+
+	@Override
+	public int getWidth() {
+		return width;
+	}
+
+	@Override
+	public int getHeight() {
+		return height;
+	}
+
+	@Override
+	public Rectangle getBounds() {
+		return new Rectangle(left, top, width, height);
+	}
+
+	@Override
+	public int getBaseline() {
+		return baseline;
+	}
+
+	@Override
+	public int getMaxWidth() {
+		return maxWidth;
+	}
+
+	@Override
+	public void setMaxWidth(final int maxWidth) {
+		this.maxWidth = maxWidth;
+	}
+
+	@Override
+	public int getInvisibleGapAtStart(final Graphics graphics) {
+		return 0;
+	}
+
+	@Override
+	public int getInvisibleGapAtEnd(final Graphics graphics) {
+		return 0;
+	}
+
+	@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 INode getNode() {
+		return node;
+	}
+
+	public void setNode(final INode node) {
+		this.node = node;
+	}
+
+	public FontSpec getFont() {
+		return fontSpec;
+	}
+
+	public void setFont(final FontSpec fontSpec) {
+		this.fontSpec = fontSpec;
+		layoutValid = false;
+	}
+
+	@Override
+	public void accept(final IBoxVisitor visitor) {
+		visitor.visit(this);
+	}
+
+	@Override
+	public <T> T accept(final IBoxVisitorWithResult<T> visitor) {
+		return visitor.visit(this);
+	}
+
+	@Override
+	public void layout(final Graphics graphics) {
+		if (layoutValid) {
+			return;
+		}
+
+		graphics.setCurrentFont(graphics.getFont(fontSpec));
+		final FontMetrics fontMetrics = graphics.getFontMetrics();
+		height = fontMetrics.getHeight();
+		baseline = fontMetrics.getAscent() + fontMetrics.getLeading();
+
+		layoutValid = true;
+	}
+
+	@Override
+	public boolean reconcileLayout(final Graphics graphics) {
+		if (layoutValid) {
+			return false;
+		}
+		layout(graphics);
+		return true;
+	}
+
+	@Override
+	public void paint(final Graphics graphics) {
+		// ignore, the box is not visible
+	}
+
+	@Override
+	public void highlight(final Graphics graphics, final Color foreground, final Color background) {
+		// ignore, the box is not visible
+	}
+
+	@Override
+	public boolean canJoin(final IInlineBox other) {
+		return false;
+	}
+
+	@Override
+	public boolean join(final IInlineBox other) {
+		return false;
+	}
+
+	@Override
+	public boolean canSplit() {
+		return false;
+	}
+
+	@Override
+	public IInlineBox splitTail(final Graphics graphics, final int headWidth, final boolean force) {
+		throw new UnsupportedOperationException("Splitting is not supported for InlineContentPlaceholder.");
+	}
+
+	@Override
+	public IContent getContent() {
+		return node.getContent();
+	}
+
+	@Override
+	public int getStartOffset() {
+		return node.getEndOffset();
+	}
+
+	@Override
+	public int getEndOffset() {
+		return node.getEndOffset();
+	}
+
+	@Override
+	public ContentRange getRange() {
+		return new ContentRange(getStartOffset(), getEndOffset());
+	}
+
+	@Override
+	public boolean isEmpty() {
+		return true;
+	}
+
+	@Override
+	public boolean isAtStart(final int offset) {
+		return getStartOffset() == offset;
+	}
+
+	@Override
+	public boolean isAtEnd(final int offset) {
+		return getEndOffset() == offset;
+	}
+
+	@Override
+	public Rectangle getPositionArea(final Graphics graphics, final int offset) {
+		if (getStartOffset() > offset || getEndOffset() < offset) {
+			return Rectangle.NULL;
+		}
+		return new Rectangle(0, 0, width, height);
+	}
+
+	@Override
+	public int getOffsetForCoordinates(final Graphics graphics, final int x, final int y) {
+		if (x < 0) {
+			return getStartOffset();
+		}
+		return getEndOffset();
+	}
+
+	@Override
+	public String toString() {
+		return "NodeEndOffsetPlaceholder [top=" + top + ", left=" + left + ", width=" + width + ", height=" + height + ", baseline=" + baseline + ", startPosition=" + getStartOffset()
+				+ ", endPosition=" + getEndOffset() + "]";
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/NodeTag.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/NodeTag.java
new file mode 100644
index 0000000..e7a95e3
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/NodeTag.java
@@ -0,0 +1,135 @@
+/*******************************************************************************
+ * 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;
+
+import org.eclipse.vex.core.internal.core.Color;
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.NodeGraphics;
+import org.eclipse.vex.core.internal.core.Point;
+import org.eclipse.vex.core.provisional.dom.INode;
+
+/**
+ * @author Florian Thienel
+ */
+public class NodeTag extends SimpleInlineBox {
+
+	public static enum Kind {
+		NODE, START, END;
+	}
+
+	private int width;
+	private int height;
+	private int baseline;
+
+	private Kind kind;
+	private INode node;
+	private Color color;
+	private boolean showText;
+
+	@Override
+	public int getWidth() {
+		return width;
+	}
+
+	@Override
+	public int getHeight() {
+		return height;
+	}
+
+	@Override
+	public int getBaseline() {
+		return baseline;
+	}
+
+	public void setKind(final Kind kind) {
+		this.kind = kind;
+	}
+
+	public void setNode(final INode node) {
+		this.node = node;
+	}
+
+	public INode getNode() {
+		return node;
+	}
+
+	public void setColor(final Color color) {
+		this.color = color;
+	}
+
+	public void setShowText(final boolean showText) {
+		this.showText = showText;
+	}
+
+	@Override
+	public void accept(final IBoxVisitor visitor) {
+		visitor.visit(this);
+	}
+
+	@Override
+	public <T> T accept(final IBoxVisitorWithResult<T> visitor) {
+		return visitor.visit(this);
+	}
+
+	@Override
+	public void layout(final Graphics graphics) {
+		final Point tagSize = getTagSize(graphics);
+		width = tagSize.getX();
+		height = tagSize.getY();
+		baseline = NodeGraphics.getTagBaseline(graphics);
+	}
+
+	private Point getTagSize(final Graphics graphics) {
+		switch (kind) {
+		case NODE:
+			return NodeGraphics.getTagSize(graphics, getText());
+		case START:
+			return NodeGraphics.getStartTagSize(graphics, getText());
+		case END:
+			return NodeGraphics.getEndTagSize(graphics, getText());
+		default:
+			throw new IllegalStateException("Unknown kind " + kind + " of NodeTag.");
+		}
+	}
+
+	private String getText() {
+		if (!showText) {
+			return "";
+		}
+		return NodeGraphics.getNodeName(node);
+	}
+
+	@Override
+	public boolean reconcileLayout(final Graphics graphics) {
+		final int oldWidth = width;
+		final int oldHeight = height;
+		layout(graphics);
+		return oldWidth != width || oldHeight != height;
+	}
+
+	@Override
+	public void paint(final Graphics graphics) {
+		graphics.setForeground(graphics.getColor(color));
+		switch (kind) {
+		case NODE:
+			NodeGraphics.drawTag(graphics, getText(), 0, 0, false, false, true);
+			break;
+		case START:
+			NodeGraphics.drawStartTag(graphics, getText(), 0, 0, false, true);
+			break;
+		case END:
+			NodeGraphics.drawEndTag(graphics, getText(), 0, 0, false, true);
+			break;
+		default:
+			throw new IllegalStateException("Unknown kind " + kind + " of NodeTag.");
+		}
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/Padding.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/Padding.java
new file mode 100644
index 0000000..7ae1ff4
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/Padding.java
@@ -0,0 +1,105 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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;
+
+import org.eclipse.vex.core.internal.core.Length;
+
+/**
+ * @author Florian Thienel
+ */
+public class Padding {
+
+	public static final Padding NULL = new Padding(0);
+
+	public final Length top;
+	public final Length left;
+	public final Length bottom;
+	public final Length right;
+
+	public Padding(final int size) {
+		this(size, size, size, size);
+	}
+
+	public Padding(final int vertical, final int horizontal) {
+		this(vertical, horizontal, vertical, horizontal);
+	}
+
+	public Padding(final int top, final int left, final int bottom, final int right) {
+		this(Length.absolute(top), Length.absolute(left), Length.absolute(bottom), Length.absolute(right));
+	}
+
+	public Padding(final Length top, final Length left, final Length bottom, final Length right) {
+		this.top = top;
+		this.left = left;
+		this.bottom = bottom;
+		this.right = right;
+	}
+
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = 1;
+		result = prime * result + (bottom == null ? 0 : bottom.hashCode());
+		result = prime * result + (left == null ? 0 : left.hashCode());
+		result = prime * result + (right == null ? 0 : right.hashCode());
+		result = prime * result + (top == null ? 0 : top.hashCode());
+		return result;
+	}
+
+	@Override
+	public boolean equals(final Object obj) {
+		if (this == obj) {
+			return true;
+		}
+		if (obj == null) {
+			return false;
+		}
+		if (getClass() != obj.getClass()) {
+			return false;
+		}
+		final Padding other = (Padding) obj;
+		if (bottom == null) {
+			if (other.bottom != null) {
+				return false;
+			}
+		} else if (!bottom.equals(other.bottom)) {
+			return false;
+		}
+		if (left == null) {
+			if (other.left != null) {
+				return false;
+			}
+		} else if (!left.equals(other.left)) {
+			return false;
+		}
+		if (right == null) {
+			if (other.right != null) {
+				return false;
+			}
+		} else if (!right.equals(other.right)) {
+			return false;
+		}
+		if (top == null) {
+			if (other.top != null) {
+				return false;
+			}
+		} else if (!top.equals(other.top)) {
+			return false;
+		}
+		return true;
+	}
+
+	@Override
+	public String toString() {
+		return "Padding [top=" + top + ", left=" + left + ", bottom=" + bottom + ", right=" + right + "]";
+	}
+
+}
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
new file mode 100644
index 0000000..95dd647
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/Paragraph.java
@@ -0,0 +1,210 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.ListIterator;
+
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+import org.eclipse.vex.core.internal.core.TextAlign;
+
+/**
+ * @author Florian Thienel
+ */
+public class Paragraph extends BaseBox implements IStructuralBox, IParentBox<IInlineBox> {
+
+	private IBox parent;
+	private int top;
+	private int left;
+	private int width;
+
+	private TextAlign textAlign = TextAlign.LEFT;
+
+	private final LinkedList<IInlineBox> children = new LinkedList<IInlineBox>();
+	private final LineArrangement lines = new LineArrangement();
+
+	@Override
+	public void setParent(final IBox parent) {
+		this.parent = parent;
+	}
+
+	@Override
+	public IBox getParent() {
+		return parent;
+	}
+
+	@Override
+	public int getAbsoluteTop() {
+		if (parent == null) {
+			return top;
+		}
+		return parent.getAbsoluteTop() + top;
+	}
+
+	@Override
+	public int getAbsoluteLeft() {
+		if (parent == null) {
+			return left;
+		}
+		return parent.getAbsoluteLeft() + left;
+	}
+
+	@Override
+	public int getTop() {
+		return top;
+	}
+
+	@Override
+	public int getLeft() {
+		return left;
+	}
+
+	@Override
+	public void setPosition(final int top, final int left) {
+		this.top = top;
+		this.left = left;
+	}
+
+	@Override
+	public int getWidth() {
+		return width;
+	}
+
+	@Override
+	public void setWidth(final int width) {
+		this.width = Math.max(0, width);
+	}
+
+	@Override
+	public int getHeight() {
+		return lines.getHeight();
+	}
+
+	@Override
+	public Rectangle getBounds() {
+		return new Rectangle(left, top, width, lines.getHeight());
+	}
+
+	public TextAlign getTextAlign() {
+		return textAlign;
+	}
+
+	public void setTextAlign(final TextAlign textAlign) {
+		this.textAlign = textAlign;
+	}
+
+	@Override
+	public void accept(final IBoxVisitor visitor) {
+		visitor.visit(this);
+	}
+
+	@Override
+	public <T> T accept(final IBoxVisitorWithResult<T> visitor) {
+		return visitor.visit(this);
+	}
+
+	public boolean hasChildren() {
+		return !children.isEmpty();
+	}
+
+	public void prependChild(final IInlineBox child) {
+		if (child == null) {
+			return;
+		}
+		if (!joinWithFirstChild(child)) {
+			child.setParent(this);
+			children.addFirst(child);
+		}
+	}
+
+	private boolean joinWithFirstChild(final IInlineBox box) {
+		if (!hasChildren()) {
+			return false;
+		}
+		final IInlineBox firstChild = children.getFirst();
+		final boolean joined = firstChild.join(box);
+		if (joined) {
+			children.removeFirst();
+			children.addFirst(box);
+		}
+		return joined;
+	}
+
+	public void appendChild(final IInlineBox child) {
+		if (child == null) {
+			return;
+		}
+		if (!joinWithLastChild(child)) {
+			child.setParent(this);
+			children.add(child);
+		}
+	}
+
+	private boolean joinWithLastChild(final IInlineBox box) {
+		if (!hasChildren()) {
+			return false;
+		}
+		final IInlineBox lastChild = children.getLast();
+		final boolean joined = lastChild.join(box);
+		return joined;
+	}
+
+	@Override
+	public void replaceChildren(final Collection<? extends IBox> oldChildren, final IInlineBox newChild) {
+		boolean newChildInserted = false;
+
+		for (final ListIterator<IInlineBox> iter = children.listIterator(); iter.hasNext();) {
+			final IInlineBox child = iter.next();
+			if (oldChildren.contains(child)) {
+				iter.remove();
+				if (!newChildInserted) {
+					iter.add(newChild);
+					newChild.setParent(this);
+					newChildInserted = true;
+				}
+			}
+		}
+	}
+
+	public Iterable<IInlineBox> getChildren() {
+		return children;
+	}
+
+	@Override
+	public void layout(final Graphics graphics) {
+		arrangeChildrenOnLines(graphics);
+	}
+
+	private void arrangeChildrenOnLines(final Graphics graphics) {
+		lines.arrangeBoxes(graphics, children.listIterator(), width, textAlign);
+	}
+
+	@Override
+	public boolean reconcileLayout(final Graphics graphics) {
+		final int oldHeight = lines.getHeight();
+		arrangeChildrenOnLines(graphics);
+		return oldHeight != lines.getHeight();
+	}
+
+	@Override
+	public void paint(final Graphics graphics) {
+		for (final Line line : lines.getLines()) {
+			/*
+			 * Line takes care of moving the origin for each child box. The coordinates of the child boxes are relative
+			 * to the Paragraph, not relative to the Line, because Paragraph is the children's parent. The Line is a
+			 * transparent utility with regards to the box structure, which is used internally by Paragraph.
+			 */
+			line.paint(graphics);
+		}
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/ParentTraversal.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/ParentTraversal.java
new file mode 100644
index 0000000..9e5ae4a
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/ParentTraversal.java
@@ -0,0 +1,117 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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 class ParentTraversal<T> implements IBoxVisitorWithResult<T> {
+
+	private final T defaultValue;
+
+	public ParentTraversal() {
+		this(null);
+	}
+
+	public ParentTraversal(final T defaultValue) {
+		this.defaultValue = defaultValue;
+	}
+
+	@Override
+	public T visit(final RootBox box) {
+		return defaultValue;
+	}
+
+	@Override
+	public T visit(final VerticalBlock box) {
+		return box.getParent().accept(this);
+	}
+
+	@Override
+	public T visit(final StructuralFrame box) {
+		return box.getParent().accept(this);
+	}
+
+	@Override
+	public T visit(final StructuralNodeReference box) {
+		return box.getParent().accept(this);
+	}
+
+	@Override
+	public T visit(final HorizontalBar box) {
+		return box.getParent().accept(this);
+	}
+
+	@Override
+	public T visit(final List box) {
+		return box.getParent().accept(this);
+	}
+
+	@Override
+	public T visit(final ListItem box) {
+		return box.getParent().accept(this);
+	}
+
+	@Override
+	public T visit(final Paragraph box) {
+		return box.getParent().accept(this);
+	}
+
+	@Override
+	public T visit(final InlineNodeReference box) {
+		return box.getParent().accept(this);
+	}
+
+	@Override
+	public T visit(final InlineContainer box) {
+		return box.getParent().accept(this);
+	}
+
+	@Override
+	public T visit(final InlineFrame box) {
+		return box.getParent().accept(this);
+	}
+
+	@Override
+	public T visit(final StaticText box) {
+		return box.getParent().accept(this);
+	}
+
+	@Override
+	public T visit(final Image box) {
+		return box.getParent().accept(this);
+	}
+
+	@Override
+	public T visit(final TextContent box) {
+		return box.getParent().accept(this);
+	}
+
+	@Override
+	public T visit(final NodeEndOffsetPlaceholder box) {
+		return box.getParent().accept(this);
+	}
+
+	@Override
+	public T visit(final GraphicalBullet box) {
+		return box.getParent().accept(this);
+	}
+
+	@Override
+	public T visit(final Square box) {
+		return box.getParent().accept(this);
+	}
+
+	@Override
+	public T visit(final NodeTag box) {
+		return box.getParent().accept(this);
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/RootBox.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/RootBox.java
new file mode 100644
index 0000000..d8fb9fd
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/RootBox.java
@@ -0,0 +1,146 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.ListIterator;
+
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+
+/**
+ * @author Florian Thienel
+ */
+public class RootBox extends BaseBox implements IParentBox<IStructuralBox> {
+
+	private int width;
+	private int height;
+	private final ArrayList<IStructuralBox> children = new ArrayList<IStructuralBox>();
+
+	@Override
+	public int getAbsoluteTop() {
+		return 0;
+	}
+
+	@Override
+	public int getAbsoluteLeft() {
+		return 0;
+	}
+
+	@Override
+	public int getTop() {
+		return 0;
+	}
+
+	@Override
+	public int getLeft() {
+		return 0;
+	}
+
+	public int getWidth() {
+		return width;
+	}
+
+	public void setWidth(final int width) {
+		this.width = Math.max(0, width);
+	}
+
+	public int getHeight() {
+		return height;
+	}
+
+	@Override
+	public Rectangle getBounds() {
+		return new Rectangle(0, 0, width, height);
+	}
+
+	public void accept(final IBoxVisitor visitor) {
+		visitor.visit(this);
+	}
+
+	@Override
+	public <T> T accept(final IBoxVisitorWithResult<T> visitor) {
+		return visitor.visit(this);
+	}
+
+	@Override
+	public boolean hasChildren() {
+		return !children.isEmpty();
+	}
+
+	@Override
+	public void prependChild(final IStructuralBox child) {
+		if (child == null) {
+			return;
+		}
+		child.setParent(this);
+		children.add(0, child);
+	}
+
+	@Override
+	public void appendChild(final IStructuralBox child) {
+		if (child == null) {
+			return;
+		}
+		child.setParent(this);
+		children.add(child);
+	}
+
+	@Override
+	public void replaceChildren(final Collection<? extends IBox> oldChildren, final IStructuralBox newChild) {
+		boolean newChildInserted = false;
+
+		for (final ListIterator<IStructuralBox> iter = children.listIterator(); iter.hasNext();) {
+			final IStructuralBox child = iter.next();
+			if (oldChildren.contains(child)) {
+				iter.remove();
+				if (!newChildInserted) {
+					iter.add(newChild);
+					newChild.setParent(this);
+					newChildInserted = true;
+				}
+			}
+		}
+	}
+
+	public Iterable<IStructuralBox> getChildren() {
+		return children;
+	}
+
+	public void layout(final Graphics graphics) {
+		height = 0;
+		for (int i = 0; i < children.size(); i += 1) {
+			final IStructuralBox child = children.get(i);
+			child.setPosition(height, 0);
+			child.setWidth(width);
+			child.layout(graphics);
+			height += child.getHeight();
+		}
+	}
+
+	@Override
+	public boolean reconcileLayout(final Graphics graphics) {
+		final int oldHeight = height;
+		height = 0;
+		for (int i = 0; i < children.size(); i += 1) {
+			final IStructuralBox child = children.get(i);
+			child.setPosition(height, 0);
+			height += child.getHeight();
+		}
+		return oldHeight != height;
+	}
+
+	@Override
+	public void paint(final Graphics graphics) {
+		ChildBoxPainter.paint(children, graphics);
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/SimpleInlineBox.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/SimpleInlineBox.java
new file mode 100644
index 0000000..36b4821
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/SimpleInlineBox.java
@@ -0,0 +1,138 @@
+/*******************************************************************************
+ * 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;
+
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+
+/**
+ * @author Florian Thienel
+ */
+public abstract class SimpleInlineBox extends BaseBox implements IInlineBox {
+
+	private IBox parent;
+	private int top;
+	private int left;
+	private int maxWidth;
+
+	private LineWrappingRule lineWrappingAtStart = LineWrappingRule.ALLOWED;
+	private LineWrappingRule lineWrappingAtEnd = LineWrappingRule.ALLOWED;
+
+	@Override
+	public void setParent(final IBox parent) {
+		this.parent = parent;
+	}
+
+	@Override
+	public IBox getParent() {
+		return parent;
+	}
+
+	@Override
+	public int getAbsoluteTop() {
+		if (parent == null) {
+			return top;
+		}
+		return parent.getAbsoluteTop() + top;
+	}
+
+	@Override
+	public int getAbsoluteLeft() {
+		if (parent == null) {
+			return left;
+		}
+		return parent.getAbsoluteLeft() + left;
+	}
+
+	@Override
+	public int getTop() {
+		return top;
+	}
+
+	@Override
+	public int getLeft() {
+		return left;
+	}
+
+	@Override
+	public void setPosition(final int top, final int left) {
+		this.top = top;
+		this.left = left;
+	}
+
+	@Override
+	public Rectangle getBounds() {
+		return new Rectangle(left, top, getWidth(), getHeight());
+	}
+
+	@Override
+	public int getMaxWidth() {
+		return maxWidth;
+	}
+
+	@Override
+	public void setMaxWidth(final int maxWidth) {
+		this.maxWidth = maxWidth;
+	}
+
+	@Override
+	public int getInvisibleGapAtStart(final Graphics graphics) {
+		return 0;
+	}
+
+	@Override
+	public int getInvisibleGapAtEnd(final Graphics graphics) {
+		return 0;
+	}
+
+	@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 boolean canJoin(final IInlineBox other) {
+		return false;
+	}
+
+	@Override
+	public boolean join(final IInlineBox other) {
+		return false;
+	}
+
+	@Override
+	public boolean canSplit() {
+		return false;
+	}
+
+	@Override
+	public IInlineBox splitTail(final Graphics graphics, final int headWidth, final boolean force) {
+		throw new UnsupportedOperationException("Splitting is not supported for " + getClass().getName() + ".");
+	}
+}
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
new file mode 100644
index 0000000..55526b4
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/Square.java
@@ -0,0 +1,71 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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;
+
+import org.eclipse.vex.core.internal.core.Color;
+import org.eclipse.vex.core.internal.core.Graphics;
+
+/**
+ * @author Florian Thienel
+ */
+public class Square extends SimpleInlineBox {
+
+	private int size;
+	private Color color;
+
+	@Override
+	public int getWidth() {
+		return size;
+	}
+
+	@Override
+	public int getHeight() {
+		return size;
+	}
+
+	@Override
+	public int getBaseline() {
+		return size;
+	}
+
+	public void setSize(final int size) {
+		this.size = size;
+	}
+
+	public void setColor(final Color color) {
+		this.color = color;
+	}
+
+	public void accept(final IBoxVisitor visitor) {
+		visitor.visit(this);
+	}
+
+	@Override
+	public <T> T accept(final IBoxVisitorWithResult<T> visitor) {
+		return visitor.visit(this);
+	}
+
+	@Override
+	public void layout(final Graphics graphics) {
+		// ignore, static size
+	}
+
+	@Override
+	public boolean reconcileLayout(final Graphics graphics) {
+		return false; // static size
+	}
+
+	@Override
+	public void paint(final Graphics graphics) {
+		graphics.setColor(graphics.getColor(color));
+		graphics.fillRect(0, 0, size, size);
+	}
+}
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
new file mode 100644
index 0000000..da7c9b4
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/StaticText.java
@@ -0,0 +1,343 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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;
+
+import static org.eclipse.vex.core.internal.core.TextUtils.countWhitespaceAtEnd;
+import static org.eclipse.vex.core.internal.core.TextUtils.countWhitespaceAtStart;
+
+import org.eclipse.vex.core.internal.core.Color;
+import org.eclipse.vex.core.internal.core.FontMetrics;
+import org.eclipse.vex.core.internal.core.FontResource;
+import org.eclipse.vex.core.internal.core.FontSpec;
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+
+/**
+ * @author Florian Thienel
+ */
+public class StaticText extends BaseBox implements IInlineBox {
+
+	private IBox parent;
+	private int top;
+	private int left;
+	private int width;
+	private int height;
+	private int baseline;
+	private int maxWidth;
+
+	private String text;
+	private FontSpec fontSpec;
+	private Color color;
+	private LineWrappingRule lineWrappingAtStart;
+	private LineWrappingRule lineWrappingAtEnd;
+
+	private final CharSequenceSplitter splitter = new CharSequenceSplitter();
+
+	private boolean layoutValid;
+
+	@Override
+	public void setParent(final IBox parent) {
+		this.parent = parent;
+	}
+
+	@Override
+	public IBox getParent() {
+		return parent;
+	}
+
+	@Override
+	public int getAbsoluteTop() {
+		if (parent == null) {
+			return top;
+		}
+		return parent.getAbsoluteTop() + top;
+	}
+
+	@Override
+	public int getAbsoluteLeft() {
+		if (parent == null) {
+			return left;
+		}
+		return parent.getAbsoluteLeft() + left;
+	}
+
+	@Override
+	public int getTop() {
+		return top;
+	}
+
+	@Override
+	public int getLeft() {
+		return left;
+	}
+
+	@Override
+	public void setPosition(final int top, final int left) {
+		this.top = top;
+		this.left = left;
+	}
+
+	@Override
+	public int getWidth() {
+		return width;
+	}
+
+	@Override
+	public int getHeight() {
+		return height;
+	}
+
+	@Override
+	public Rectangle getBounds() {
+		return new Rectangle(left, top, width, height);
+	}
+
+	@Override
+	public int getBaseline() {
+		return baseline;
+	}
+
+	@Override
+	public int getMaxWidth() {
+		return maxWidth;
+	}
+
+	@Override
+	public void setMaxWidth(final int maxWidth) {
+		this.maxWidth = maxWidth;
+	}
+
+	public int getInvisibleGapAtStart(final Graphics graphics) {
+		final String text = renderText(getText());
+		final int whitespaceCount = countWhitespaceAtStart(text);
+		return graphics.stringWidth(text.substring(0, whitespaceCount));
+	}
+
+	public int getInvisibleGapAtEnd(final Graphics graphics) {
+		final String text = renderText(getText());
+		final int whitespaceCount = countWhitespaceAtEnd(text);
+		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;
+	}
+
+	public void setText(final String text) {
+		this.text = text;
+		layoutValid = false;
+	}
+
+	public FontSpec getFont() {
+		return fontSpec;
+	}
+
+	public void setFont(final FontSpec fontSpec) {
+		this.fontSpec = fontSpec;
+		layoutValid = false;
+	}
+
+	public Color getColor() {
+		return color;
+	}
+
+	public void setColor(final Color color) {
+		this.color = color;
+		layoutValid = false;
+	}
+
+	@Override
+	public void accept(final IBoxVisitor visitor) {
+		visitor.visit(this);
+	}
+
+	@Override
+	public <T> T accept(final IBoxVisitorWithResult<T> visitor) {
+		return visitor.visit(this);
+	}
+
+	@Override
+	public void layout(final Graphics graphics) {
+		if (layoutValid) {
+			return;
+		}
+
+		applyFont(graphics);
+		width = graphics.stringWidth(renderText(getText()));
+
+		final FontMetrics fontMetrics = graphics.getFontMetrics();
+		height = fontMetrics.getHeight();
+		baseline = fontMetrics.getAscent() + fontMetrics.getLeading();
+
+		layoutValid = true;
+	}
+
+	private static String renderText(final String rawText) {
+		return rawText.replaceAll("\n", " ").replaceAll("\t", "    ");
+	}
+
+	@Override
+	public boolean reconcileLayout(final Graphics graphics) {
+		final int oldHeight = height;
+		final int oldWidth = width;
+		final int oldBaseline = baseline;
+
+		layout(graphics);
+
+		return oldHeight != height || oldWidth != width || oldBaseline != baseline;
+	}
+
+	@Override
+	public void paint(final Graphics graphics) {
+		applyFont(graphics);
+		graphics.setColor(graphics.getColor(color));
+		graphics.drawString(renderText(getText()), 0, 0);
+	}
+
+	private void applyFont(final Graphics graphics) {
+		final FontResource font = graphics.getFont(fontSpec);
+		graphics.setCurrentFont(font);
+	}
+
+	@Override
+	public boolean canJoin(final IInlineBox other) {
+		if (!(other instanceof StaticText)) {
+			return false;
+		}
+		if (!lineSplittingRulesAllowJoining((StaticText) other)) {
+			return false;
+		}
+		if (!hasEqualFont((StaticText) other)) {
+			return false;
+		}
+		if (!hasEqualColor((StaticText) other)) {
+			return false;
+		}
+
+		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;
+		}
+		if (fontSpec == null && other.fontSpec != null) {
+			return false;
+		}
+
+		return true;
+	}
+
+	private boolean hasEqualColor(final StaticText other) {
+		if (color != null && !color.equals(other.color)) {
+			return false;
+		}
+		if (color == null && other.color != null) {
+			return false;
+		}
+		return true;
+	}
+
+	@Override
+	public boolean join(final IInlineBox other) {
+		if (!canJoin(other)) {
+			return false;
+		}
+		final StaticText otherText = (StaticText) other;
+
+		setText(text + otherText.text);
+		width += otherText.width;
+
+		return true;
+	}
+
+	@Override
+	public boolean canSplit() {
+		return true;
+	}
+
+	@Override
+	public IInlineBox splitTail(final Graphics graphics, final int headWidth, final boolean force) {
+		applyFont(graphics);
+		splitter.setContent(text);
+		final int splittingPosition = splitter.findSplittingPositionBefore(graphics, headWidth, width, force);
+
+		final StaticText tail = createTail(splittingPosition);
+		tail.layout(graphics);
+		removeTail(tail);
+
+		adjustSplittingRules(tail);
+
+		return tail;
+	}
+
+	private StaticText createTail(final int splittingPosition) {
+		final StaticText tail = new StaticText();
+		if (splittingPosition < text.length()) {
+			tail.setText(text.substring(Math.min(splittingPosition, text.length()), text.length()));
+		} else {
+			tail.setText("");
+		}
+		tail.setFont(fontSpec);
+		tail.setColor(color);
+		tail.setParent(parent);
+		return tail;
+	}
+
+	private void removeTail(final StaticText tail) {
+		final int headLength = text.length() - tail.text.length();
+		text = text.substring(0, headLength);
+		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/StructuralFrame.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/StructuralFrame.java
new file mode 100644
index 0000000..8c05e4e
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/StructuralFrame.java
@@ -0,0 +1,233 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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;
+
+import org.eclipse.vex.core.internal.core.Color;
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+
+/**
+ * @author Florian Thienel
+ */
+public class StructuralFrame extends BaseBox implements IStructuralBox, IDecoratorBox<IStructuralBox> {
+
+	private IBox parent;
+	private int top;
+	private int left;
+	private int width;
+	private int height;
+
+	private Margin margin = Margin.NULL;
+	private Border border = Border.NULL;
+	private Padding padding = Padding.NULL;
+	private Color backgroundColor = null;
+
+	private IStructuralBox component;
+
+	@Override
+	public void setParent(final IBox parent) {
+		this.parent = parent;
+	}
+
+	@Override
+	public IBox getParent() {
+		return parent;
+	}
+
+	@Override
+	public int getAbsoluteTop() {
+		if (parent == null) {
+			return top;
+		}
+		return parent.getAbsoluteTop() + top;
+	}
+
+	@Override
+	public int getAbsoluteLeft() {
+		if (parent == null) {
+			return left;
+		}
+		return parent.getAbsoluteLeft() + left;
+	}
+
+	public int getTop() {
+		return top;
+	}
+
+	public int getLeft() {
+		return left;
+	}
+
+	public void setPosition(final int top, final int left) {
+		this.top = top;
+		this.left = left;
+	}
+
+	@Override
+	public int getWidth() {
+		return width;
+	}
+
+	public void setWidth(final int width) {
+		this.width = Math.max(0, width);
+	}
+
+	@Override
+	public int getHeight() {
+		return height;
+	}
+
+	@Override
+	public Rectangle getBounds() {
+		return new Rectangle(left, top, width, height);
+	}
+
+	@Override
+	public void accept(final IBoxVisitor visitor) {
+		visitor.visit(this);
+	}
+
+	@Override
+	public <T> T accept(final IBoxVisitorWithResult<T> visitor) {
+		return visitor.visit(this);
+	}
+
+	public Margin getMargin() {
+		return margin;
+	}
+
+	public void setMargin(final Margin margin) {
+		this.margin = margin;
+	}
+
+	public Border getBorder() {
+		return border;
+	}
+
+	public void setBorder(final Border border) {
+		this.border = border;
+	}
+
+	public Padding getPadding() {
+		return padding;
+	}
+
+	public void setPadding(final Padding padding) {
+		this.padding = padding;
+	}
+
+	public Color getBackgroundColor() {
+		return backgroundColor;
+	}
+
+	public void setBackgroundColor(final Color backgroundColor) {
+		this.backgroundColor = backgroundColor;
+	}
+
+	public void setComponent(final IStructuralBox component) {
+		this.component = component;
+		component.setParent(this);
+	}
+
+	@Override
+	public IStructuralBox getComponent() {
+		return component;
+	}
+
+	@Override
+	public void layout(final Graphics graphics) {
+		if (component == null) {
+			return;
+		}
+
+		layoutComponent(graphics);
+
+		height = component.getHeight();
+		height += topFrame(component.getHeight());
+		height += bottomFrame(component.getHeight());
+	}
+
+	private void layoutComponent(final Graphics graphics) {
+		final int componentWidth = width - (leftFrame() + rightFrame());
+		component.setWidth(componentWidth);
+		component.layout(graphics);
+		component.setPosition(topFrame(component.getHeight()), leftFrame());
+	}
+
+	private int topFrame(final int componentHeight) {
+		return margin.top.get(componentHeight) + border.top.width + padding.top.get(componentHeight);
+	}
+
+	private int leftFrame() {
+		return margin.left.get(width) + border.left.width + padding.left.get(width);
+	}
+
+	private int bottomFrame(final int componentHeight) {
+		return margin.bottom.get(componentHeight) + border.bottom.width + padding.bottom.get(componentHeight);
+	}
+
+	private int rightFrame() {
+		return margin.right.get(width) + border.right.width + padding.right.get(width);
+	}
+
+	@Override
+	public boolean reconcileLayout(final Graphics graphics) {
+		final int oldHeight = height;
+
+		height = component.getHeight();
+		height += topFrame(component.getHeight());
+		height += bottomFrame(component.getHeight());
+
+		return oldHeight != height;
+	}
+
+	@Override
+	public void paint(final Graphics graphics) {
+		drawBackground(graphics);
+		drawBorder(graphics);
+		paintComponent(graphics);
+	}
+
+	private void drawBackground(final Graphics graphics) {
+		if (backgroundColor == null) {
+			return;
+		}
+
+		graphics.setBackground(graphics.getColor(backgroundColor));
+		graphics.fillRect(0, 0, width, height);
+	}
+
+	private void drawBorder(final Graphics graphics) {
+		final int rectTop = margin.top.get(component.getHeight()) + border.top.width / 2;
+		final int rectLeft = margin.left.get(width) + border.left.width / 2;
+		final int rectBottom = height - margin.bottom.get(component.getHeight()) - border.bottom.width / 2;
+		final int rectRight = width - margin.right.get(width) - border.right.width / 2;
+
+		drawBorderLine(graphics, border.top, rectTop, rectLeft - border.left.width / 2, rectTop, rectRight + border.right.width / 2);
+		drawBorderLine(graphics, border.left, rectTop - border.top.width / 2, rectLeft, rectBottom + border.bottom.width / 2, rectLeft);
+		drawBorderLine(graphics, border.bottom, rectBottom, rectLeft - border.left.width / 2, rectBottom, rectRight + border.right.width / 2);
+		drawBorderLine(graphics, border.right, rectTop - border.top.width / 2, rectRight, rectBottom + border.bottom.width / 2, rectRight);
+	}
+
+	private void drawBorderLine(final Graphics graphics, final BorderLine borderLine, final int top, final int left, final int bottom, final int right) {
+		if (borderLine.width <= 0) {
+			return;
+		}
+		graphics.setLineStyle(borderLine.style);
+		graphics.setLineWidth(borderLine.width);
+		graphics.setColor(graphics.getColor(borderLine.color));
+		graphics.drawLine(left, top, right, bottom);
+	}
+
+	private void paintComponent(final Graphics graphics) {
+		ChildBoxPainter.paint(component, graphics);
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/StructuralNodeReference.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/StructuralNodeReference.java
new file mode 100644
index 0000000..161c315
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/StructuralNodeReference.java
@@ -0,0 +1,339 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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;
+
+import org.eclipse.vex.core.internal.core.Color;
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.LineStyle;
+import org.eclipse.vex.core.internal.core.NodeGraphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+import org.eclipse.vex.core.provisional.dom.ContentRange;
+import org.eclipse.vex.core.provisional.dom.IContent;
+import org.eclipse.vex.core.provisional.dom.INode;
+
+/**
+ * @author Florian Thienel
+ */
+public class StructuralNodeReference extends BaseBox implements IStructuralBox, IDecoratorBox<IStructuralBox>, IContentBox {
+
+	private static final float HIGHLIGHT_LIGHTEN_AMOUNT = 0.6f;
+	private static final int HIGHLIGHT_BORDER_WIDTH = 4;
+
+	private IBox parent;
+	private int top;
+	private int left;
+	private int width;
+	private int height;
+
+	private IStructuralBox component;
+
+	private INode node;
+	private boolean canContainText;
+	private boolean containsInlineContent;
+
+	@Override
+	public void setParent(final IBox parent) {
+		this.parent = parent;
+	}
+
+	@Override
+	public IBox getParent() {
+		return parent;
+	}
+
+	@Override
+	public int getAbsoluteTop() {
+		if (parent == null) {
+			return top;
+		}
+		return parent.getAbsoluteTop() + top;
+	}
+
+	@Override
+	public int getAbsoluteLeft() {
+		if (parent == null) {
+			return left;
+		}
+		return parent.getAbsoluteLeft() + left;
+	}
+
+	@Override
+	public int getTop() {
+		return top;
+	}
+
+	@Override
+	public int getLeft() {
+		return left;
+	}
+
+	@Override
+	public void setPosition(final int top, final int left) {
+		this.top = top;
+		this.left = left;
+	}
+
+	@Override
+	public int getWidth() {
+		return width;
+	}
+
+	@Override
+	public void setWidth(final int width) {
+		this.width = Math.max(0, width);
+	}
+
+	@Override
+	public int getHeight() {
+		return height;
+	}
+
+	@Override
+	public Rectangle getBounds() {
+		return new Rectangle(left, top, width, height);
+	}
+
+	@Override
+	public void accept(final IBoxVisitor visitor) {
+		visitor.visit(this);
+	}
+
+	@Override
+	public <T> T accept(final IBoxVisitorWithResult<T> visitor) {
+		return visitor.visit(this);
+	}
+
+	@Override
+	public void setComponent(final IStructuralBox component) {
+		this.component = component;
+		component.setParent(this);
+	}
+
+	@Override
+	public IStructuralBox getComponent() {
+		return component;
+	}
+
+	public void setNode(final INode node) {
+		this.node = node;
+	}
+
+	public INode getNode() {
+		return node;
+	}
+
+	public void setCanContainText(final boolean canContainText) {
+		this.canContainText = canContainText;
+	}
+
+	public boolean canContainText() {
+		return canContainText;
+	}
+
+	public void setContainsInlineContent(final boolean containsInlineContent) {
+		this.containsInlineContent = containsInlineContent;
+	}
+
+	public boolean containsInlineContent() {
+		return containsInlineContent;
+	}
+
+	@Override
+	public void layout(final Graphics graphics) {
+		if (component == null) {
+			return;
+		}
+		component.setPosition(0, 0);
+		component.setWidth(width);
+		component.layout(graphics);
+		height = component.getHeight();
+	}
+
+	@Override
+	public boolean reconcileLayout(final Graphics graphics) {
+		final int oldHeight = height;
+		height = component.getHeight();
+		return oldHeight != height;
+	}
+
+	@Override
+	public void paint(final Graphics graphics) {
+		ChildBoxPainter.paint(component, graphics);
+	}
+
+	public void highlight(final Graphics graphics, final Color foreground, final Color background) {
+		final Color lightBackground = background.lighten(HIGHLIGHT_LIGHTEN_AMOUNT);
+		fillBackground(graphics, lightBackground);
+		drawBorder(graphics, background);
+
+		accept(new DepthFirstBoxTraversal<Object>() {
+			@Override
+			public Object visit(final StructuralNodeReference box) {
+				if (box != StructuralNodeReference.this) {
+					box.highlightInside(graphics, foreground, lightBackground);
+				}
+				return super.visit(box);
+			}
+
+			@Override
+			public Object visit(final InlineNodeReference box) {
+				box.highlightInside(graphics, foreground, lightBackground);
+				return super.visit(box);
+			}
+
+			@Override
+			public Object visit(final TextContent box) {
+				box.highlight(graphics, foreground, lightBackground);
+				return super.visit(box);
+			}
+
+			@Override
+			public Object visit(final NodeEndOffsetPlaceholder box) {
+				box.highlight(graphics, foreground, lightBackground);
+				return super.visit(box);
+			}
+
+			@Override
+			public Object visit(final NodeTag box) {
+				paintBox(graphics, box);
+				return super.visit(box);
+			}
+
+			@Override
+			public Object visit(final GraphicalBullet box) {
+				paintBox(graphics, box);
+				return super.visit(box);
+			}
+
+			@Override
+			public Object visit(final Square box) {
+				paintBox(graphics, box);
+				return super.visit(box);
+			}
+
+			@Override
+			public Object visit(final StaticText box) {
+				paintBox(graphics, box);
+				return super.visit(box);
+			}
+
+			@Override
+			public Object visit(final Image box) {
+				paintBox(graphics, box);
+				return super.visit(box);
+			}
+
+			private void paintBox(final Graphics graphics, final IBox box) {
+				graphics.moveOrigin(box.getAbsoluteLeft(), box.getAbsoluteTop());
+				box.paint(graphics);
+				graphics.moveOrigin(-box.getAbsoluteLeft(), -box.getAbsoluteTop());
+			}
+		});
+
+		drawTag(graphics, foreground, background);
+	}
+
+	private void fillBackground(final Graphics graphics, final Color color) {
+		graphics.setForeground(graphics.getColor(color));
+		graphics.setBackground(graphics.getColor(color));
+		graphics.fillRect(getAbsoluteLeft(), getAbsoluteTop(), width, height);
+	}
+
+	private void drawBorder(final Graphics graphics, final Color color) {
+		graphics.setForeground(graphics.getColor(color));
+		graphics.setBackground(graphics.getColor(color));
+		graphics.setLineStyle(LineStyle.SOLID);
+		graphics.setLineWidth(HIGHLIGHT_BORDER_WIDTH);
+		graphics.drawRect(getAbsoluteLeft(), getAbsoluteTop(), width, height);
+	}
+
+	private void drawTag(final Graphics graphics, final Color foreground, final Color background) {
+		graphics.setForeground(graphics.getColor(foreground));
+		graphics.setBackground(graphics.getColor(background));
+		NodeGraphics.drawTag(graphics, node, getAbsoluteLeft() + width / 2, getAbsoluteTop() + height / 2, true, true, false);
+	}
+
+	public void highlightInside(final Graphics graphics, final Color foreground, final Color background) {
+		fillBackground(graphics, background);
+	}
+
+	@Override
+	public IContent getContent() {
+		return node.getContent();
+	}
+
+	@Override
+	public int getStartOffset() {
+		if (node == null) {
+			return 0;
+		}
+		return node.getStartOffset();
+	}
+
+	@Override
+	public int getEndOffset() {
+		if (node == null) {
+			return 0;
+		}
+		return node.getEndOffset();
+	}
+
+	@Override
+	public ContentRange getRange() {
+		if (node == null) {
+			return ContentRange.NULL;
+		}
+		return node.getRange();
+	}
+
+	@Override
+	public Rectangle getPositionArea(final Graphics graphics, final int offset) {
+		return new Rectangle(0, 0, width, height);
+	}
+
+	@Override
+	public int getOffsetForCoordinates(final Graphics graphics, final int x, final int y) {
+		if (isEmpty()) {
+			return getEndOffset();
+		}
+
+		final int half = height / 2;
+		if (y < half) {
+			return getStartOffset();
+		} else {
+			return getEndOffset();
+		}
+	}
+
+	@Override
+	public boolean isEmpty() {
+		return getEndOffset() - getStartOffset() <= 1;
+	}
+
+	public boolean isAtStart(final int offset) {
+		return getStartOffset() == offset;
+	}
+
+	public boolean isAtEnd(final int offset) {
+		return getEndOffset() == offset;
+	}
+
+	@Override
+	public String toString() {
+		String result = "StructuralNodeReference{ ";
+		result += "x: " + left + ", y: " + top + ", width: " + width + ", height: " + height;
+		if (node != null) {
+			result += ", startOffset: " + node.getStartOffset() + ", endOffset: " + node.getEndOffset();
+		}
+		result += " }";
+		return result;
+	}
+}
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
new file mode 100644
index 0000000..b450579
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/TextContent.java
@@ -0,0 +1,508 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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;
+
+import static org.eclipse.vex.core.internal.core.TextUtils.countWhitespaceAtEnd;
+import static org.eclipse.vex.core.internal.core.TextUtils.countWhitespaceAtStart;
+
+import org.eclipse.core.runtime.Assert;
+import org.eclipse.vex.core.internal.core.Color;
+import org.eclipse.vex.core.internal.core.FontMetrics;
+import org.eclipse.vex.core.internal.core.FontResource;
+import org.eclipse.vex.core.internal.core.FontSpec;
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+import org.eclipse.vex.core.provisional.dom.ContentRange;
+import org.eclipse.vex.core.provisional.dom.IContent;
+import org.eclipse.vex.core.provisional.dom.IPosition;
+
+/**
+ * @author Florian Thienel
+ */
+public class TextContent extends BaseBox implements IInlineBox, IContentBox {
+
+	private IBox parent;
+	private int top;
+	private int left;
+	private int width;
+	private int height;
+	private int baseline;
+	private int maxWidth;
+	private LineWrappingRule lineWrappingAtStart;
+	private LineWrappingRule lineWrappingAtEnd;
+
+	private IContent content;
+	private IPosition startPosition;
+	private IPosition endPosition;
+
+	private FontSpec fontSpec;
+	private Color color;
+
+	private final CharSequenceSplitter splitter = new CharSequenceSplitter();
+
+	private boolean layoutValid;
+	private int layoutStartOffset;
+	private int layoutEndOffset;
+
+	@Override
+	public void setParent(final IBox parent) {
+		this.parent = parent;
+	}
+
+	@Override
+	public IBox getParent() {
+		return parent;
+	}
+
+	@Override
+	public int getAbsoluteTop() {
+		if (parent == null) {
+			return top;
+		}
+		return parent.getAbsoluteTop() + top;
+	}
+
+	@Override
+	public int getAbsoluteLeft() {
+		if (parent == null) {
+			return left;
+		}
+		return parent.getAbsoluteLeft() + left;
+	}
+
+	@Override
+	public int getTop() {
+		return top;
+	}
+
+	@Override
+	public int getLeft() {
+		return left;
+	}
+
+	@Override
+	public void setPosition(final int top, final int left) {
+		this.top = top;
+		this.left = left;
+	}
+
+	@Override
+	public int getWidth() {
+		return width;
+	}
+
+	@Override
+	public int getHeight() {
+		return height;
+	}
+
+	@Override
+	public Rectangle getBounds() {
+		return new Rectangle(left, top, width, height);
+	}
+
+	@Override
+	public int getBaseline() {
+		return baseline;
+	}
+
+	@Override
+	public int getMaxWidth() {
+		return maxWidth;
+	}
+
+	@Override
+	public void setMaxWidth(final int maxWidth) {
+		this.maxWidth = maxWidth;
+	}
+
+	public String getText() {
+		return content.getText(new ContentRange(startPosition.getOffset(), endPosition.getOffset()));
+	}
+
+	private static String renderText(final String rawText) {
+		return rawText.replaceAll("\n", " ").replaceAll("\t", "    ");
+	}
+
+	public int getInvisibleGapAtStart(final Graphics graphics) {
+		final String text = renderText(getText());
+		final int whitespaceCount = countWhitespaceAtStart(text);
+		return graphics.stringWidth(text.substring(0, whitespaceCount));
+	}
+
+	public int getInvisibleGapAtEnd(final Graphics graphics) {
+		final String text = renderText(getText());
+		final int whitespaceCount = countWhitespaceAtEnd(text);
+		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;
+	}
+
+	private void validateLayout() {
+		layoutStartOffset = startPosition.getOffset();
+		layoutEndOffset = endPosition.getOffset();
+		layoutValid = true;
+	}
+
+	private boolean isLayoutValid() {
+		return layoutValid && layoutStartOffset == startPosition.getOffset() && layoutEndOffset == endPosition.getOffset();
+	}
+
+	public void setContent(final IContent content, final ContentRange range) {
+		this.content = content;
+		startPosition = content.createPosition(range.getStartOffset());
+		endPosition = content.createPosition(range.getEndOffset());
+		invalidateLayout();
+	}
+
+	public FontSpec getFont() {
+		return fontSpec;
+	}
+
+	public void setFont(final FontSpec fontSpec) {
+		this.fontSpec = fontSpec;
+		invalidateLayout();
+	}
+
+	public Color getColor() {
+		return color;
+	}
+
+	public void setColor(final Color color) {
+		this.color = color;
+		invalidateLayout();
+	}
+
+	@Override
+	public void accept(final IBoxVisitor visitor) {
+		visitor.visit(this);
+	}
+
+	@Override
+	public <T> T accept(final IBoxVisitorWithResult<T> visitor) {
+		return visitor.visit(this);
+	}
+
+	@Override
+	public void layout(final Graphics graphics) {
+		if (isLayoutValid()) {
+			return;
+		}
+
+		applyFont(graphics);
+		width = graphics.stringWidth(renderText(getText()));
+
+		final FontMetrics fontMetrics = graphics.getFontMetrics();
+		height = fontMetrics.getHeight();
+		baseline = fontMetrics.getAscent() + fontMetrics.getLeading();
+
+		validateLayout();
+	}
+
+	@Override
+	public boolean reconcileLayout(final Graphics graphics) {
+		final int oldHeight = height;
+		final int oldWidth = width;
+		final int oldBaseline = baseline;
+
+		layout(graphics);
+
+		return oldHeight != height || oldWidth != width || oldBaseline != baseline;
+	}
+
+	@Override
+	public void paint(final Graphics graphics) {
+		applyFont(graphics);
+		graphics.setForeground(graphics.getColor(color));
+		graphics.drawString(renderText(getText()), 0, 0);
+	}
+
+	private void applyFont(final Graphics graphics) {
+		final FontResource font = graphics.getFont(fontSpec);
+		graphics.setCurrentFont(font);
+	}
+
+	@Override
+	public void highlight(final Graphics graphics, final Color foreground, final Color background) {
+		graphics.setForeground(graphics.getColor(foreground));
+		graphics.setBackground(graphics.getColor(background));
+		graphics.fillRect(getAbsoluteLeft(), getAbsoluteTop(), width, height);
+		applyFont(graphics);
+		graphics.drawString(renderText(getText()), getAbsoluteLeft(), getAbsoluteTop());
+	}
+
+	public void highlight(final Graphics graphics, final int startOffset, final int endOffset, final Color foreground, final Color background) {
+		final int highlightStartOffset = Math.max(getStartOffset(), Math.min(startOffset, getEndOffset())) - getStartOffset();
+		final int highlightEndOffset = Math.max(getStartOffset(), Math.min(endOffset, getEndOffset() + 1)) - getStartOffset();
+		final String text = getText();
+		final String highlightPrefix = renderText(text.substring(0, highlightStartOffset));
+		final String highlightText = renderText(text.substring(highlightStartOffset, highlightEndOffset));
+
+		applyFont(graphics);
+		final int widthBefore = graphics.stringWidth(highlightPrefix);
+		final int widthHighlight = graphics.stringWidth(highlightText);
+
+		graphics.setForeground(graphics.getColor(foreground));
+		graphics.setBackground(graphics.getColor(background));
+		graphics.fillRect(getAbsoluteLeft() + widthBefore, getAbsoluteTop(), widthHighlight, height);
+		graphics.drawString(highlightText, getAbsoluteLeft() + widthBefore, getAbsoluteTop());
+	}
+
+	@Override
+	public boolean canJoin(final IInlineBox other) {
+		if (!(other instanceof TextContent)) {
+			return false;
+		}
+		if (!isAdjacent((TextContent) other)) {
+			return false;
+		}
+		if (!lineSplittingRulesAllowJoining((TextContent) other)) {
+			return false;
+		}
+		if (!hasEqualFont((TextContent) other)) {
+			return false;
+		}
+		if (!hasEqualColor((TextContent) other)) {
+			return false;
+		}
+
+		return true;
+	}
+
+	private boolean isAdjacent(final TextContent other) {
+		if (content != other.content) {
+			return false;
+		}
+		if (endPosition.getOffset() != other.startPosition.getOffset() - 1) {
+			return false;
+		}
+		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;
+		}
+		if (fontSpec == null && other.fontSpec != null) {
+			return false;
+		}
+
+		return true;
+	}
+
+	private boolean hasEqualColor(final TextContent other) {
+		if (color != null && !color.equals(other.color)) {
+			return false;
+		}
+		if (color == null && other.color != null) {
+			return false;
+		}
+		return true;
+	}
+
+	@Override
+	public boolean join(final IInlineBox other) {
+		if (!canJoin(other)) {
+			return false;
+		}
+		final TextContent otherText = (TextContent) other;
+
+		content.removePosition(endPosition);
+		content.removePosition(otherText.startPosition);
+		endPosition = otherText.endPosition;
+		width += otherText.width;
+
+		lineWrappingAtEnd = otherText.lineWrappingAtEnd;
+
+		return true;
+	}
+
+	@Override
+	public boolean canSplit() {
+		return true;
+	}
+
+	@Override
+	public IInlineBox splitTail(final Graphics graphics, final int headWidth, final boolean force) {
+		applyFont(graphics);
+		splitter.setContent(content, startPosition.getOffset(), endPosition.getOffset());
+		final int splittingPosition = splitter.findSplittingPositionBefore(graphics, headWidth, width, force);
+		final int splittingOffset = startPosition.getOffset() + splittingPosition;
+		if (splittingOffset > endPosition.getOffset()) {
+			return new TextContent();
+		}
+
+		final TextContent tail = createTail(splittingOffset);
+		tail.layout(graphics);
+		removeTail(tail);
+
+		adjustSplittingRules(tail);
+
+		return tail;
+	}
+
+	private TextContent createTail(final int splittingOffset) {
+		final TextContent tail = new TextContent();
+		tail.setContent(content, new ContentRange(splittingOffset, endPosition.getOffset()));
+		tail.setFont(fontSpec);
+		tail.setColor(color);
+		tail.setParent(parent);
+		return tail;
+	}
+
+	private void removeTail(final TextContent tail) {
+		final int headLength = endPosition.getOffset() - startPosition.getOffset() - (tail.endPosition.getOffset() - tail.startPosition.getOffset());
+		content.removePosition(endPosition);
+		endPosition = content.createPosition(startPosition.getOffset() + headLength - 1);
+		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 IContent getContent() {
+		return content;
+	}
+
+	@Override
+	public int getStartOffset() {
+		return startPosition.getOffset();
+	}
+
+	public void setStartOffset(final int startOffset) {
+		Assert.isTrue(endPosition == null || startOffset <= endPosition.getOffset(), "startPosition > endPosition");
+		content.removePosition(startPosition);
+		startPosition = content.createPosition(startOffset);
+		invalidateLayout();
+	}
+
+	@Override
+	public int getEndOffset() {
+		return endPosition.getOffset();
+	}
+
+	public void setEndOffset(final int endOffset) {
+		Assert.isTrue(startPosition == null || endOffset >= startPosition.getOffset(), "endPosition < startPosition");
+		content.removePosition(endPosition);
+		endPosition = content.createPosition(endOffset);
+		invalidateLayout();
+	}
+
+	@Override
+	public ContentRange getRange() {
+		return new ContentRange(getStartOffset(), getEndOffset());
+	}
+
+	@Override
+	public boolean isEmpty() {
+		return getEndOffset() - getStartOffset() <= 0;
+	}
+
+	public boolean isAtStart(final int offset) {
+		return getStartOffset() == offset;
+	}
+
+	public boolean isAtEnd(final int offset) {
+		return getEndOffset() == offset;
+	}
+
+	@Override
+	public Rectangle getPositionArea(final Graphics graphics, final int offset) {
+		if (startPosition.getOffset() > offset || endPosition.getOffset() < offset) {
+			return Rectangle.NULL;
+		}
+
+		applyFont(graphics);
+		final char c = content.charAt(offset);
+		final String head = renderText(content.subSequence(startPosition.getOffset(), offset).toString());
+		final int left = graphics.stringWidth(head);
+		final int charWidth = graphics.stringWidth(renderText(Character.toString(c)));
+		return new Rectangle(left, 0, charWidth, height);
+	}
+
+	@Override
+	public int getOffsetForCoordinates(final Graphics graphics, final int x, final int y) {
+		if (x < 0) {
+			return getStartOffset();
+		}
+		if (x > width) {
+			return getEndOffset();
+		}
+
+		applyFont(graphics);
+		final String text = getText();
+		int i = 0;
+		while (renderedWidth(graphics, text.substring(0, i)) < x && i < text.length()) {
+			i += 1;
+		}
+		final int offset = Math.max(getStartOffset(), getStartOffset() + i - 1);
+
+		final Rectangle area = getPositionArea(graphics, offset);
+		final int halfWidth = area.getWidth() / 2 + 1;
+		if (x < area.getX() + halfWidth) {
+			return offset;
+		} else {
+			return Math.min(offset + 1, getEndOffset());
+		}
+	}
+
+	private static int renderedWidth(final Graphics graphics, final String text) {
+		return graphics.stringWidth(renderText(text));
+	}
+
+	@Override
+	public String toString() {
+		return "TextContent{ x: " + left + ", y: " + top + ", width: " + width + ", height: " + height + ", startOffset: " + startPosition + ", endOffset: " + endPosition + " }";
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/VerticalBlock.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/VerticalBlock.java
new file mode 100644
index 0000000..43c6324
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/VerticalBlock.java
@@ -0,0 +1,171 @@
+/*******************************************************************************
+ * Copyright (c) 2014 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;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.ListIterator;
+
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+
+/**
+ * This box arranges child boxes in one vertical column of given width. Its height depends on the sum of the height of
+ * its children.
+ *
+ * @author Florian Thienel
+ */
+public class VerticalBlock extends BaseBox implements IStructuralBox, IParentBox<IStructuralBox> {
+
+	private IBox parent;
+	private int top;
+	private int left;
+	private int width;
+	private int height;
+	private final ArrayList<IStructuralBox> children = new ArrayList<IStructuralBox>();
+
+	@Override
+	public void setParent(final IBox parent) {
+		this.parent = parent;
+	}
+
+	@Override
+	public IBox getParent() {
+		return parent;
+	}
+
+	@Override
+	public int getAbsoluteTop() {
+		if (parent == null) {
+			return top;
+		}
+		return parent.getAbsoluteTop() + top;
+	}
+
+	@Override
+	public int getAbsoluteLeft() {
+		if (parent == null) {
+			return left;
+		}
+		return parent.getAbsoluteLeft() + left;
+	}
+
+	public int getTop() {
+		return top;
+	}
+
+	public int getLeft() {
+		return left;
+	}
+
+	public void setPosition(final int top, final int left) {
+		this.top = top;
+		this.left = left;
+	}
+
+	@Override
+	public int getWidth() {
+		return width;
+	}
+
+	public void setWidth(final int width) {
+		this.width = Math.max(0, width);
+	}
+
+	@Override
+	public int getHeight() {
+		return height;
+	}
+
+	@Override
+	public Rectangle getBounds() {
+		return new Rectangle(left, top, width, height);
+	}
+
+	@Override
+	public void accept(final IBoxVisitor visitor) {
+		visitor.visit(this);
+	}
+
+	@Override
+	public <T> T accept(final IBoxVisitorWithResult<T> visitor) {
+		return visitor.visit(this);
+	}
+
+	public boolean hasChildren() {
+		return !children.isEmpty();
+	}
+
+	public void prependChild(final IStructuralBox child) {
+		if (child == null) {
+			return;
+		}
+		child.setParent(this);
+		children.add(0, child);
+	}
+
+	public void appendChild(final IStructuralBox child) {
+		if (child == null) {
+			return;
+		}
+		child.setParent(this);
+		children.add(child);
+	}
+
+	@Override
+	public void replaceChildren(final Collection<? extends IBox> oldChildren, final IStructuralBox newChild) {
+		boolean newChildInserted = false;
+
+		for (final ListIterator<IStructuralBox> iter = children.listIterator(); iter.hasNext();) {
+			final IStructuralBox child = iter.next();
+			if (oldChildren.contains(child)) {
+				iter.remove();
+				if (!newChildInserted) {
+					iter.add(newChild);
+					newChild.setParent(this);
+					newChildInserted = true;
+				}
+			}
+		}
+	}
+
+	public Iterable<IStructuralBox> getChildren() {
+		return children;
+	}
+
+	public void layout(final Graphics graphics) {
+		height = 0;
+		for (int i = 0; i < children.size(); i += 1) {
+			final IStructuralBox child = children.get(i);
+			child.setPosition(height, 0);
+			child.setWidth(width);
+			child.layout(graphics);
+			height += child.getHeight();
+		}
+	}
+
+	@Override
+	public boolean reconcileLayout(final Graphics graphics) {
+		final int oldHeight = height;
+		height = 0;
+		for (int i = 0; i < children.size(); i += 1) {
+			final IStructuralBox child = children.get(i);
+			child.setPosition(height, 0);
+			height += child.getHeight();
+		}
+		return oldHeight != height;
+	}
+
+	@Override
+	public void paint(final Graphics graphics) {
+		ChildBoxPainter.paint(children, graphics);
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/Color.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/Color.java
index 6208b58..f3d65ae 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/Color.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/Color.java
@@ -17,14 +17,16 @@
 public class Color {
 
 	public static final Color BLACK = new Color(0, 0, 0);
+	public static final Color WHITE = new Color(255, 255, 255);
+	public static final Color RED = new Color(255, 0, 0);
+	public static final Color GREEN = new Color(0, 255, 0);
+	public static final Color BLUE = new Color(0, 0, 255);
 
 	private final int red;
 	private final int green;
 	private final int blue;
 
 	/**
-	 * Class constructor.
-	 *
 	 * @param red
 	 *            red value, 0..255
 	 * @param green
@@ -59,6 +61,13 @@
 		return red;
 	}
 
+	public Color lighten(final float amount) {
+		final int lighterRed = Math.round(Math.min(2