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(255.0f, red + 255.0f * amount));
+		final int lighterGreen = Math.round(Math.min(255.0f, green + 255.0f * amount));
+		final int lighterBlue = Math.round(Math.min(255.0f, blue + 255.0f * amount));
+		return new Color(lighterRed, lighterGreen, lighterBlue);
+	}
+
 	@Override
 	public boolean equals(final Object obj) {
 		if (this == obj) {
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/DisplayDevice.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/DisplayDevice.java
index 4406f49..136e86d 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/DisplayDevice.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/DisplayDevice.java
@@ -27,6 +27,18 @@
 		}
 	};
 
+	public static final DisplayDevice _72DPI = new DisplayDevice() {
+		@Override
+		public int getHorizontalPPI() {
+			return 72;
+		}
+
+		@Override
+		public int getVerticalPPI() {
+			return 72;
+		}
+	};
+
 	/**
 	 * Class constructor.
 	 */
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/FontSpec.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/FontSpec.java
index 62badec..b90d6dc 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/FontSpec.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/FontSpec.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2004, 2008 John Krasnay and others.
+ * Copyright (c) 2004, 2014 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
@@ -8,6 +8,7 @@
  * Contributors:
  *     John Krasnay - initial API and implementation
  *     Dave Holroyd - Implement text decoration
+ *     Florian Thienel - add convenience constructor with only one name
  *******************************************************************************/
 package org.eclipse.vex.core.internal.core;
 
@@ -51,6 +52,10 @@
 		this.size = size;
 	}
 
+	public FontSpec(final String name, final int style, final float size) {
+		this(new String[] { name }, style, size);
+	}
+
 	/**
 	 * Returns the names of the font families that match the font.
 	 */
@@ -78,6 +83,14 @@
 		return style;
 	}
 
+	public FontSpec bold() {
+		return new FontSpec(names, style | BOLD, size);
+	}
+
+	public FontSpec italic() {
+		return new FontSpec(names, style | ITALIC, size);
+	}
+
 	@Override
 	public String toString() {
 		return "FontSpec [names=" + Arrays.toString(names) + ", size=" + size + ", style=" + styleToString(style) + "]";
@@ -104,4 +117,39 @@
 			builder.append(representation);
 		}
 	}
+
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = 1;
+		result = prime * result + Arrays.hashCode(names);
+		result = prime * result + Float.floatToIntBits(size);
+		result = prime * result + style;
+		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 FontSpec other = (FontSpec) obj;
+		if (!Arrays.equals(names, other.names)) {
+			return false;
+		}
+		if (Float.floatToIntBits(size) != Float.floatToIntBits(other.size)) {
+			return false;
+		}
+		if (style != other.style) {
+			return false;
+		}
+		return true;
+	}
+
 }
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/Graphics.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/Graphics.java
index 4b2f819..77025de 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/Graphics.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/Graphics.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2004, 2010 John Krasnay and others.
+ * Copyright (c) 2004, 2014 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
@@ -8,6 +8,7 @@
  * Contributors:
  *     John Krasnay - initial API and implementation
  *     Mohamadou Nassourou - Bug 298912 - rudimentary support for images
+ *     Florian Thienel - foreground, background
  *******************************************************************************/
 package org.eclipse.vex.core.internal.core;
 
@@ -19,51 +20,55 @@
  */
 public interface Graphics {
 
-	public static final int LINE_SOLID = 0;
-	public static final int LINE_DASH = 1;
-	public static final int LINE_DOT = 2;
-
 	public int charsWidth(char[] data, int offset, int length);
 
-	public ColorResource createColor(Color rgb);
+	public ColorResource getColor(Color rgb);
 
-	public FontResource createFont(FontSpec fontSpec);
+	public FontResource getFont(FontSpec fontSpec);
 
 	public void dispose();
 
+	public void resetOrigin();
+
+	public void moveOrigin(int offsetX, int offsetY);
+
+	public int asAbsoluteX(int relativeX);
+
+	public int asAbsoluteY(int relativeY);
+
+	public int asRelativeX(int absoluteX);
+
+	public int asRelativeY(int absoluteY);
+
 	public void drawChars(char[] chars, int offset, int length, int x, int y);
 
 	public void drawLine(int x1, int y1, int x2, int y2);
 
-	/**
-	 * Draw the given string at the given point using the current font.
-	 *
-	 * @param s
-	 *            string to draw
-	 * @param x
-	 *            x-coordinate of the top left corner of the text box
-	 * @param y
-	 *            y-coordinate of the top left corner of the text box
-	 */
 	public void drawString(String s, int x, int y);
 
 	public void drawOval(int x, int y, int width, int height);
 
 	public void drawRect(int x, int y, int width, int height);
 
+	public void drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight);
+
+	public void drawPolygon(int... coordinates);
+
 	public void drawImage(Image image, int x, int y, int width, int height);
 
 	public void fillOval(int x, int y, int width, int height);
 
 	public void fillRect(int x, int y, int width, int height);
 
+	public void fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight);
+
+	public void fillPolygon(int... coordinates);
+
 	public Rectangle getClipBounds();
 
-	public ColorResource getColor();
+	public FontResource getCurrentFont();
 
-	public FontResource getFont();
-
-	public int getLineStyle();
+	public LineStyle getLineStyle();
 
 	public int getLineWidth();
 
@@ -77,11 +82,23 @@
 
 	public void setAntiAliased(boolean antiAliased);
 
+	public ColorResource getColor();
+
 	public ColorResource setColor(ColorResource color);
 
-	public FontResource setFont(FontResource font);
+	public ColorResource getForeground();
 
-	public void setLineStyle(int style);
+	public ColorResource setForeground(ColorResource color);
+
+	public ColorResource getBackground();
+
+	public ColorResource setBackground(ColorResource color);
+
+	public void swapColors();
+
+	public FontResource setCurrentFont(FontResource font);
+
+	public void setLineStyle(LineStyle style);
 
 	public void setLineWidth(int width);
 
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/Length.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/Length.java
new file mode 100644
index 0000000..ce0a5b8
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/Length.java
@@ -0,0 +1,109 @@
+/*******************************************************************************
+ * Copyright (c) 2004, 2016 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
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     John Krasnay - initial API and implementation
+ *     Florian Thienel - generalize for use outside of the CSS package
+ *******************************************************************************/
+package org.eclipse.vex.core.internal.core;
+
+/**
+ * A length that may be expressed as an absolute or relative value.
+ */
+public abstract class Length {
+
+	public static final Length ZERO = new Length() {
+		@Override
+		public int get(final int referenceLength) {
+			return 0;
+		}
+
+		@Override
+		public int getBaseValue() {
+			return 0;
+		}
+	};
+
+	/**
+	 * @return a length representing an absolute value.
+	 */
+	public static Length absolute(final int value) {
+		return new Length() {
+			@Override
+			public int get(final int referenceLength) {
+				return value;
+			}
+
+			@Override
+			public int getBaseValue() {
+				return value;
+			}
+		};
+	}
+
+	/**
+	 * @return a length representing a relative value.
+	 */
+	public static Length relative(final float percentage) {
+		return new Length() {
+			@Override
+			public int get(final int referenceLength) {
+				return Math.round(percentage * referenceLength);
+			}
+
+			@Override
+			public int getBaseValue() {
+				return Float.floatToIntBits(percentage);
+			}
+		};
+	}
+
+	/**
+	 * Return the value of the length given a reference value. If this object represents an absolute value, that value
+	 * is simply returned. Otherwise, returns the given reference length multiplied by the given percentage and rounded
+	 * to the nearest integer.
+	 *
+	 * @param referenceLength
+	 *            reference length by which percentage lengths will by multiplied.
+	 * @return the actual value
+	 */
+	public abstract int get(final int referenceLength);
+
+	public abstract int getBaseValue();
+
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = 1;
+		result = prime * result + getBaseValue();
+		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 Length other = (Length) obj;
+		if (getBaseValue() != other.getBaseValue()) {
+			return false;
+		}
+		return true;
+	}
+
+	@Override
+	public String toString() {
+		return "Length [baseValue=" + getBaseValue() + "]";
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/LineStyle.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/LineStyle.java
new file mode 100644
index 0000000..b2249af
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/LineStyle.java
@@ -0,0 +1,20 @@
+/*******************************************************************************
+ * 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;
+
+/**
+ * @author Florian Thienel
+ */
+public enum LineStyle {
+
+	SOLID, DOTTED, DASHED;
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/NodeGraphics.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/NodeGraphics.java
new file mode 100644
index 0000000..7780895
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/NodeGraphics.java
@@ -0,0 +1,208 @@
+/*******************************************************************************
+ * 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 org.eclipse.vex.core.provisional.dom.BaseNodeVisitorWithResult;
+import org.eclipse.vex.core.provisional.dom.IComment;
+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.eclipse.vex.core.provisional.dom.INode;
+import org.eclipse.vex.core.provisional.dom.IProcessingInstruction;
+
+/**
+ * @author Florian Thienel
+ */
+public class NodeGraphics {
+
+	private static final FontSpec FONT = new FontSpec("Arial", FontSpec.BOLD, 10.0f);
+	private static final int TEXT_PADDING = 3;
+	private static final int MARGIN = 2;
+
+	public static void drawTag(final Graphics graphics, final INode node, final int x, final int y, final boolean horizontallyCentered, final boolean verticallyCentered, final boolean transparent) {
+		drawTag(graphics, getNodeName(node), x, y, horizontallyCentered, verticallyCentered, transparent);
+	}
+
+	public static void drawTag(final Graphics graphics, final String text, final int x, final int y, final boolean horizontallyCentered, final boolean verticallyCentered, final boolean transparent) {
+		graphics.setCurrentFont(graphics.getFont(FONT));
+		final int textWidth = graphics.stringWidth(text) + TEXT_PADDING * 2;
+		final int textHeight = graphics.getFontMetrics().getHeight() + TEXT_PADDING * 2;
+		final int arc = textHeight / 3;
+
+		final int effectiveX;
+		if (horizontallyCentered) {
+			effectiveX = x - textWidth / 2 - MARGIN;
+		} else {
+			effectiveX = x;
+		}
+
+		final int effectiveY;
+		if (verticallyCentered) {
+			effectiveY = y - textHeight / 2 - MARGIN;
+		} else {
+			effectiveY = y;
+		}
+
+		if (!transparent) {
+			graphics.fillRoundRect(effectiveX, effectiveY, textWidth + MARGIN * 2 - 1, textHeight + MARGIN * 2 - 1, arc, arc);
+		}
+		graphics.setLineStyle(LineStyle.SOLID);
+		graphics.setLineWidth(1);
+		graphics.drawRoundRect(effectiveX + MARGIN - 1, effectiveY + MARGIN - 1, textWidth, textHeight, arc, arc);
+		graphics.drawString(text, effectiveX + TEXT_PADDING + MARGIN - 1, effectiveY + TEXT_PADDING + MARGIN - 1);
+	}
+
+	public static Point getTagSize(final Graphics graphics, final INode node) {
+		return getTagSize(graphics, getNodeName(node));
+	}
+
+	public static Point getTagSize(final Graphics graphics, final String text) {
+		graphics.setCurrentFont(graphics.getFont(FONT));
+		final int width = graphics.stringWidth(text) + (TEXT_PADDING + MARGIN) * 2 + 1;
+		final int height = graphics.getFontMetrics().getHeight() + (TEXT_PADDING + MARGIN) * 2 + 1;
+		return new Point(width, height);
+	}
+
+	public static int getTagBaseline(final Graphics graphics) {
+		graphics.setCurrentFont(graphics.getFont(FONT));
+		final FontMetrics fontMetrics = graphics.getFontMetrics();
+		return fontMetrics.getAscent() + fontMetrics.getLeading() + TEXT_PADDING + MARGIN;
+	}
+
+	public static void drawStartTag(final Graphics graphics, final INode node, final int x, final int y, final boolean verticallyCentered, final boolean transparent) {
+		drawStartTag(graphics, getNodeName(node), x, y, verticallyCentered, false);
+	}
+
+	public static void drawStartTag(final Graphics graphics, final String text, final int x, final int y, final boolean verticallyCentered, final boolean transparent) {
+		graphics.setCurrentFont(graphics.getFont(FONT));
+		final int textWidth = graphics.stringWidth(text) + TEXT_PADDING;
+		final int textHeight = graphics.getFontMetrics().getHeight() + TEXT_PADDING * 2;
+
+		final int effectiveY;
+		if (verticallyCentered) {
+			effectiveY = y - textHeight / 2 - MARGIN;
+		} else {
+			effectiveY = y;
+		}
+
+		if (!transparent) {
+			graphics.fillPolygon(arrowRight(x, effectiveY, textWidth + 2 * MARGIN, textHeight + MARGIN * 2 - 1));
+		}
+
+		graphics.setLineStyle(LineStyle.SOLID);
+		graphics.setLineWidth(1);
+		graphics.drawPolygon(arrowRight(x + MARGIN, effectiveY + MARGIN - 1, textWidth, textHeight));
+
+		graphics.drawString(text, x + TEXT_PADDING + MARGIN, effectiveY + TEXT_PADDING + MARGIN - 1);
+	}
+
+	private static int[] arrowRight(final int x, final int y, final int width, final int height) {
+		final int arrow = height / 2;
+		return new int[] {
+				x, y,
+				x + width, y,
+				x + width + arrow, y + height / 2,
+				x + width, y + height,
+				x, y + height
+		};
+	}
+
+	public static Point getStartTagSize(final Graphics graphics, final INode node) {
+		return getStartTagSize(graphics, getNodeName(node));
+	}
+
+	public static Point getStartTagSize(final Graphics graphics, final String text) {
+		graphics.setCurrentFont(graphics.getFont(FONT));
+		final int height = graphics.getFontMetrics().getHeight() + (TEXT_PADDING + MARGIN) * 2;
+		final int width = graphics.stringWidth(text) + TEXT_PADDING + MARGIN * 2 + height / 2;
+		return new Point(width, height);
+	}
+
+	public static void drawEndTag(final Graphics graphics, final INode node, final int x, final int y, final boolean verticallyCentered, final boolean transparent) {
+		drawEndTag(graphics, getNodeName(node), x, y, verticallyCentered, false);
+	}
+
+	public static void drawEndTag(final Graphics graphics, final String text, final int x, final int y, final boolean verticallyCentered, final boolean transparent) {
+		graphics.setCurrentFont(graphics.getFont(FONT));
+		final int textWidth = graphics.stringWidth(text) + TEXT_PADDING;
+		final int textHeight = graphics.getFontMetrics().getHeight() + TEXT_PADDING * 2;
+		final int arrow = textHeight / 2 + MARGIN;
+
+		final int effectiveY;
+		if (verticallyCentered) {
+			effectiveY = y - textHeight / 2 - MARGIN;
+		} else {
+			effectiveY = y;
+		}
+
+		if (!transparent) {
+			graphics.fillPolygon(arrowLeft(x, effectiveY, textWidth + MARGIN + 1, textHeight + MARGIN * 2 - 1));
+		}
+
+		graphics.setLineStyle(LineStyle.SOLID);
+		graphics.setLineWidth(1);
+		graphics.drawPolygon(arrowLeft(x + MARGIN, effectiveY + MARGIN - 1, textWidth, textHeight));
+
+		graphics.drawString(text, x + arrow, effectiveY + TEXT_PADDING + MARGIN - 1);
+	}
+
+	private static int[] arrowLeft(final int x, final int y, final int width, final int height) {
+		final int arrow = height / 2;
+		return new int[] {
+				x, y + height / 2,
+				x + arrow, y,
+				x + arrow + width, y,
+				x + arrow + width, y + height,
+				x + arrow, y + height
+		};
+	}
+
+	public static Point getEndTagSize(final Graphics graphics, final INode node) {
+		return getStartTagSize(graphics, getNodeName(node));
+	}
+
+	public static Point getEndTagSize(final Graphics graphics, final String text) {
+		graphics.setCurrentFont(graphics.getFont(FONT));
+		final int height = graphics.getFontMetrics().getHeight() + (TEXT_PADDING + MARGIN) * 2;
+		final int width = graphics.stringWidth(text) + TEXT_PADDING + MARGIN * 2 + height / 2;
+		return new Point(width, height);
+	}
+
+	public static String getNodeName(final INode node) {
+		return node.accept(new BaseNodeVisitorWithResult<String>() {
+			@Override
+			public String visit(final IDocument document) {
+				return "DOCUMENT";
+			}
+
+			@Override
+			public String visit(final IElement element) {
+				return element.getPrefixedName();
+			}
+
+			@Override
+			public String visit(final IComment comment) {
+				return "COMMENT";
+			}
+
+			@Override
+			public String visit(final IProcessingInstruction pi) {
+				return "?" + pi.getTarget();
+			}
+
+			@Override
+			public String visit(final IIncludeNode include) {
+				return getNodeName(include.getReference());
+			}
+		});
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/Point.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/Point.java
index 20f906d..cf5c0aa 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/Point.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/Point.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2004, 2008 John Krasnay and others.
+ * Copyright (c) 2004, 2016 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
@@ -7,6 +7,7 @@
  *
  * Contributors:
  *     John Krasnay - initial API and implementation
+ *     Florian Thienel - hashCode, equals, toString
  *******************************************************************************/
 package org.eclipse.vex.core.internal.core;
 
@@ -19,8 +20,6 @@
 	private final int y;
 
 	/**
-	 * Class constructor.
-	 *
 	 * @param x
 	 *            X-coordinate.
 	 * @param y
@@ -31,30 +30,52 @@
 		this.y = y;
 	}
 
-	@Override
-	public String toString() {
-		final StringBuffer sb = new StringBuffer(80);
-		sb.append(Point.class.getName());
-		sb.append("[x=");
-		sb.append(getX());
-		sb.append(",y=");
-		sb.append(getY());
-		sb.append("]");
-		return sb.toString();
-	}
-
 	/**
-	 * Returns the x-coordinate.
+	 * @return the x-coordinate.
 	 */
 	public int getX() {
 		return x;
 	}
 
 	/**
-	 * Returns the y-coordinate.
+	 * @return the y-coordinate.
 	 */
 	public int getY() {
 		return y;
 	}
 
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = 1;
+		result = prime * result + x;
+		result = prime * result + y;
+		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 Point other = (Point) obj;
+		if (x != other.x) {
+			return false;
+		}
+		if (y != other.y) {
+			return false;
+		}
+		return true;
+	}
+
+	@Override
+	public String toString() {
+		return "Point [x=" + x + ", y=" + y + "]";
+	}
 }
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/Rectangle.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/Rectangle.java
index 9096e98..210e7c9 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/Rectangle.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/Rectangle.java
@@ -15,6 +15,8 @@
  */
 public class Rectangle {
 
+	public static final Rectangle NULL = new Rectangle(0, 0, 0, 0);
+
 	private final int x;
 	private final int y;
 	private final int width;
@@ -31,6 +33,14 @@
 		return rect.x < x + width && rect.x + rect.width > x && rect.y < y + height && rect.y + rect.height > y;
 	}
 
+	public boolean below(final Rectangle rect) {
+		return y >= rect.y + rect.height;
+	}
+
+	public boolean above(final Rectangle rect) {
+		return y + height <= rect.y;
+	}
+
 	@Override
 	public String toString() {
 		final StringBuffer sb = new StringBuffer(80);
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/TextAlign.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/TextAlign.java
new file mode 100644
index 0000000..20e2011
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/TextAlign.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.core;
+
+/**
+ * @author Florian Thienel
+ */
+public enum TextAlign {
+	LEFT, CENTER, RIGHT;
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/TextUtils.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/TextUtils.java
new file mode 100644
index 0000000..9b0f23f
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/core/TextUtils.java
@@ -0,0 +1,53 @@
+/*******************************************************************************
+ * 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 java.util.regex.Pattern;
+
+import org.eclipse.vex.core.XML;
+
+public class TextUtils {
+
+	public static final char CURRENCY_SIGN = '\u00A4';
+	public static final char PARAGRAPH_SIGN = '\u00B6';
+	public static final char RAQUO = '\u00BB';
+
+	public static final Pattern ANY_LINE_BREAKS = Pattern.compile("(\r\n|\r|\n)");
+
+	public static int countWhitespaceAtStart(final String text) {
+		int whitespaceCount = 0;
+		while (whitespaceCount < text.length()) {
+			if (XML.isWhitespace(text.charAt(whitespaceCount))) {
+				whitespaceCount += 1;
+			} else {
+				break;
+			}
+		}
+		return whitespaceCount;
+	}
+
+	public static int countWhitespaceAtEnd(final String text) {
+		int whitespaceCount = 0;
+		while (whitespaceCount < text.length()) {
+			final int i = text.length() - 1 - whitespaceCount;
+			if (XML.isWhitespace(text.charAt(i))) {
+				whitespaceCount += 1;
+			} else {
+				break;
+			}
+		}
+		return whitespaceCount;
+	}
+
+	public static String[] lines(final String s) {
+		return ANY_LINE_BREAKS.split(s, -1);
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/AlphabeticNumeral.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/AlphabeticNumeral.java
new file mode 100644
index 0000000..51b4ba1
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/AlphabeticNumeral.java
@@ -0,0 +1,92 @@
+/*******************************************************************************
+ * 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;
+
+/**
+ * @author Florian Thienel
+ */
+public class AlphabeticNumeral {
+
+	private static final char[] LATIN_ALPHABET = "abcdefghijklmnopqrstuvwxyz".toCharArray();
+	private static final char[] GREEK_ALPHABET = "αβγδεζηθικλμνξοπρστυφχψω".toCharArray();
+
+	private final int value;
+	private final String numeral;
+
+	public static AlphabeticNumeral toLatin(final int value) {
+		return new AlphabeticNumeral(LATIN_ALPHABET, value);
+	}
+
+	public static AlphabeticNumeral toGreek(final int value) {
+		return new AlphabeticNumeral(GREEK_ALPHABET, value);
+	}
+
+	public AlphabeticNumeral(final char[] alphabet, final int value) {
+		this.value = value;
+		numeral = numeral(alphabet, value);
+	}
+
+	private static String numeral(final char[] alphabet, final int value) {
+		if (value <= 0) {
+			return "";
+		}
+		final StringBuilder numeral = new StringBuilder();
+
+		int i = value;
+		while (i > 0) {
+			final int remainder = (i - 1) % alphabet.length;
+			numeral.insert(0, alphabet[remainder]);
+			i = (i - 1) / alphabet.length;
+		}
+
+		return numeral.toString();
+	}
+
+	public int intValue() {
+		return value;
+	}
+
+	@Override
+	public String toString() {
+		return numeral;
+	}
+
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = 1;
+		result = prime * result + (numeral == null ? 0 : numeral.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 AlphabeticNumeral other = (AlphabeticNumeral) obj;
+		if (numeral == null) {
+			if (other.numeral != null) {
+				return false;
+			}
+		} else if (!numeral.equals(other.numeral)) {
+			return false;
+		}
+		return true;
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/AttributeDependendContent.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/AttributeDependendContent.java
new file mode 100644
index 0000000..289424d
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/AttributeDependendContent.java
@@ -0,0 +1,40 @@
+/*******************************************************************************
+ * 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 org.eclipse.core.runtime.QualifiedName;
+import org.eclipse.vex.core.provisional.dom.IElement;
+
+public class AttributeDependendContent implements IPropertyContent {
+
+	public final IElement element;
+	public final QualifiedName attributeName;
+
+	public AttributeDependendContent(final IElement element, final QualifiedName attributeName) {
+		this.element = element;
+		this.attributeName = attributeName;
+	}
+
+	@Override
+	public <T> T accept(final IPropertyContentVisitor<T> visitor) {
+		return visitor.visit(this);
+	}
+
+	@Override
+	public String toString() {
+		final String attributeValue = element.getAttributeValue(attributeName);
+		if (attributeValue != null) {
+			return attributeValue;
+		}
+		return "";
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/BulletStyle.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/BulletStyle.java
new file mode 100644
index 0000000..69f2713
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/BulletStyle.java
@@ -0,0 +1,135 @@
+/*******************************************************************************
+ * 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 org.eclipse.vex.core.internal.core.Image;
+
+public class BulletStyle {
+
+	public final Type type;
+	public final Position position;
+	public final Image image;
+	public final char character;
+
+	public static BulletStyle fromStyles(final Styles styles) {
+		return new BulletStyle(getBulletType(styles), Position.OUTSIDE, null, '\0');
+	}
+
+	private static Type getBulletType(final Styles styles) {
+		final String listStyleType = styles.getListStyleType();
+		if (CSS.DECIMAL.equals(listStyleType)) {
+			return Type.DECIMAL;
+		} else if (CSS.DECIMAL_LEADING_ZERO.equals(listStyleType)) {
+			return Type.DECIMAL_LEADING_ZERO;
+		} else if (CSS.LOWER_ROMAN.equals(listStyleType)) {
+			return Type.LOWER_ROMAN;
+		} else if (CSS.UPPER_ROMAN.equals(listStyleType)) {
+			return Type.UPPER_ROMAN;
+		} else if (CSS.LOWER_ALPHA.equals(listStyleType)) {
+			return Type.LOWER_LATIN;
+		} else if (CSS.UPPER_ALPHA.equals(listStyleType)) {
+			return Type.UPPER_LATIN;
+		} else if (CSS.LOWER_LATIN.equals(listStyleType)) {
+			return Type.LOWER_LATIN;
+		} else if (CSS.UPPER_LATIN.equals(listStyleType)) {
+			return Type.UPPER_LATIN;
+		} else if (CSS.LOWER_GREEK.equals(listStyleType)) {
+			return Type.LOWER_GREEK;
+		} else if (CSS.DISC.equals(listStyleType)) {
+			return Type.DISC;
+		} else if (CSS.CIRCLE.equals(listStyleType)) {
+			return Type.CIRCLE;
+		} else if (CSS.SQUARE.equals(listStyleType)) {
+			return Type.SQUARE;
+		} else {
+			return Type.NONE;
+		}
+	}
+
+	public BulletStyle(final Type type, final Position position, final Image image, final char character) {
+		this.type = type;
+		this.position = position;
+		this.image = image;
+		this.character = character;
+	}
+
+	public String getBulletAsText(final int zeroBasedIndex, final int itemCount) {
+		switch (type) {
+		case DECIMAL:
+			return toDecimal(zeroBasedIndex);
+		case DECIMAL_LEADING_ZERO:
+			return toDecimalWithLeadingZeroes(zeroBasedIndex, itemCount);
+		case LOWER_ROMAN:
+			return toLowerRoman(zeroBasedIndex);
+		case UPPER_ROMAN:
+			return toUpperRoman(zeroBasedIndex);
+		case LOWER_LATIN:
+			return toLowerLatin(zeroBasedIndex);
+		case UPPER_LATIN:
+			return toUpperLatin(zeroBasedIndex);
+		case LOWER_GREEK:
+			return toLowerGreek(zeroBasedIndex);
+		default:
+			return "";
+		}
+	}
+
+	public static String toDecimal(final int zeroBasedIndex) {
+		return String.format("%d.", zeroBasedIndex + 1);
+	}
+
+	public static String toDecimalWithLeadingZeroes(final int zeroBasedIndex, final int itemCount) {
+		final int digitCount = Integer.toString(itemCount).length();
+		return String.format("%0" + digitCount + "d.", zeroBasedIndex + 1);
+	}
+
+	public static String toLowerRoman(final int zeroBasedIndex) {
+		return toUpperRoman(zeroBasedIndex).toLowerCase();
+	}
+
+	public static String toUpperRoman(final int zeroBasedIndex) {
+		return new RomanNumeral(zeroBasedIndex + 1).toString() + ".";
+	}
+
+	public static String toLowerLatin(final int zeroBasedIndex) {
+		return AlphabeticNumeral.toLatin(zeroBasedIndex + 1).toString() + ".";
+	}
+
+	public static String toUpperLatin(final int zeroBasedIndex) {
+		return toLowerLatin(zeroBasedIndex).toUpperCase();
+	}
+
+	public static String toLowerGreek(final int zeroBasedIndex) {
+		return AlphabeticNumeral.toGreek(zeroBasedIndex + 1).toString() + ".";
+	}
+
+	public static enum Type {
+		NONE, DECIMAL, DECIMAL_LEADING_ZERO, LOWER_ROMAN, UPPER_ROMAN, LOWER_LATIN, UPPER_LATIN, LOWER_GREEK, DISC, CIRCLE, SQUARE;
+
+		public boolean isTextual() {
+			if (this == NONE) {
+				return false;
+			}
+			return ordinal() < DISC.ordinal();
+		}
+
+		public boolean isGraphical() {
+			if (this == NONE) {
+				return false;
+			}
+			return ordinal() >= DISC.ordinal();
+		}
+	}
+
+	public static enum Position {
+		INSIDE, OUTSIDE;
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/CSS.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/CSS.java
index a146a86..627a5c9 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/CSS.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/CSS.java
@@ -135,6 +135,9 @@
 	public static final String OUTLINE_CONTENT = "_vex-outline-content";
 	public static final String INLINE_MARKER = "_vex-inline-marker";
 
+	// VEX specific functions
+	public static final String IMAGE_FUNCTION = "image";
+
 	// suffixes to BORDER_XXX
 	public static final String COLOR_SUFFIX = "-color";
 	public static final String STYLE_SUFFIX = "-style";
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/ContentProperty.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/ContentProperty.java
new file mode 100644
index 0000000..ea8b4e3
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/ContentProperty.java
@@ -0,0 +1,145 @@
+/*******************************************************************************
+ * 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 java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.core.runtime.QualifiedName;
+import org.eclipse.vex.core.provisional.dom.BaseNodeVisitorWithResult;
+import org.eclipse.vex.core.provisional.dom.IElement;
+import org.eclipse.vex.core.provisional.dom.INode;
+import org.eclipse.vex.core.provisional.dom.IProcessingInstruction;
+import org.w3c.css.sac.LexicalUnit;
+
+public class ContentProperty extends AbstractProperty {
+
+	public ContentProperty() {
+		super(CSS.CONTENT);
+	}
+
+	@Override
+	public List<IPropertyContent> calculate(final LexicalUnit lu, final Styles parentStyles, final Styles styles, final INode node) {
+		final List<IPropertyContent> result = new ArrayList<IPropertyContent>();
+
+		LexicalUnit currentLexicalUnit = lu;
+		while (currentLexicalUnit != null) {
+			IPropertyContent propertyContent;
+			switch (currentLexicalUnit.getLexicalUnitType()) {
+			case LexicalUnit.SAC_STRING_VALUE:
+				propertyContent = stringValue(currentLexicalUnit, result);
+				break;
+			case LexicalUnit.SAC_ATTR:
+				propertyContent = attr(currentLexicalUnit, node, result);
+				break;
+			case LexicalUnit.SAC_FUNCTION:
+				if (CSS.IMAGE_FUNCTION.equalsIgnoreCase(currentLexicalUnit.getFunctionName())) {
+					String stylesBaseURI;
+					if (styles != null && styles.getBaseUrl() != null) {
+						stylesBaseURI = styles.getBaseUrl().toString();
+					} else {
+						stylesBaseURI = null;
+					}
+					propertyContent = image(currentLexicalUnit, node, stylesBaseURI);
+				} else {
+					propertyContent = null;
+				}
+				break;
+			default:
+				// ignore other LexicalUnit types
+				propertyContent = null;
+				break;
+			}
+
+			if (propertyContent != null) {
+				result.add(propertyContent);
+			}
+
+			currentLexicalUnit = currentLexicalUnit.getNextLexicalUnit();
+		}
+
+		return result;
+	}
+
+	private static IPropertyContent stringValue(final LexicalUnit lexicalUnit, final List<IPropertyContent> result) {
+		return new TextualContent(lexicalUnit.getStringValue());
+	}
+
+	private static IPropertyContent attr(final LexicalUnit currentLexicalUnit, final INode node, final List<IPropertyContent> result) {
+		final String stringValue = currentLexicalUnit.getStringValue();
+		return node.accept(new BaseNodeVisitorWithResult<IPropertyContent>() {
+			@Override
+			public IPropertyContent visit(final IElement element) {
+				return new AttributeDependendContent(element, new QualifiedName(null, stringValue));
+			}
+
+			@Override
+			public IPropertyContent visit(final IProcessingInstruction pi) {
+				if (CSS.PSEUDO_TARGET.equalsIgnoreCase(stringValue)) {
+					return new ProcessingInstructionTargetContent(pi);
+				}
+				return null;
+			}
+		});
+	}
+
+	private static IPropertyContent uri(final LexicalUnit lexicalUnit) {
+		return new URIContent(lexicalUnit.getStringValue());
+	}
+
+	private static IPropertyContent image(final LexicalUnit lexicalUnit, final INode node, final String stylesBaseURI) {
+		final List<IPropertyContent> parameters = parseImageParameters(lexicalUnit.getParameters(), node);
+		final String baseURI = determineImageBaseURI(parameters, node, stylesBaseURI);
+
+		return new ImageContent(baseURI, parameters);
+	}
+
+	private static List<IPropertyContent> parseImageParameters(final LexicalUnit parameters, final INode node) {
+		final List<IPropertyContent> result = new ArrayList<IPropertyContent>();
+
+		LexicalUnit currentLexicalUnit = parameters;
+		while (currentLexicalUnit != null) {
+			IPropertyContent propertyContent;
+			switch (currentLexicalUnit.getLexicalUnitType()) {
+			case LexicalUnit.SAC_STRING_VALUE:
+				propertyContent = stringValue(currentLexicalUnit, result);
+				break;
+			case LexicalUnit.SAC_ATTR:
+				propertyContent = attr(currentLexicalUnit, node, result);
+				break;
+			case LexicalUnit.SAC_URI:
+				propertyContent = uri(currentLexicalUnit);
+				break;
+			default:
+				// ignore other LexicalUnit types
+				propertyContent = null;
+				break;
+			}
+
+			if (propertyContent != null) {
+				result.add(propertyContent);
+			}
+
+			currentLexicalUnit = currentLexicalUnit.getNextLexicalUnit();
+		}
+
+		return result;
+	}
+
+	private static String determineImageBaseURI(final List<IPropertyContent> parameters, final INode node, final String stylesBaseURI) {
+		for (final IPropertyContent parameter : parameters) {
+			if (parameter instanceof AttributeDependendContent) {
+				return node.getBaseURI();
+			}
+		}
+		return stylesBaseURI;
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/CssWhitespacePolicy.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/CssWhitespacePolicy.java
index c92d260..8790e0b 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/CssWhitespacePolicy.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/CssWhitespacePolicy.java
@@ -48,6 +48,9 @@
 
 	@Override
 	public boolean isBlock(final INode node) {
+		if (node == null) {
+			return true;
+		}
 		return node.accept(new BaseNodeVisitorWithResult<Boolean>(true) {
 			@Override
 			public Boolean visit(final IElement element) {
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/IPropertyContent.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/IPropertyContent.java
new file mode 100644
index 0000000..2eb6441
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/IPropertyContent.java
@@ -0,0 +1,16 @@
+/*******************************************************************************
+ * 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;
+
+public interface IPropertyContent {
+
+	<T> T accept(IPropertyContentVisitor<T> visitor);
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/IPropertyContentVisitor.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/IPropertyContentVisitor.java
new file mode 100644
index 0000000..c9664c5
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/IPropertyContentVisitor.java
@@ -0,0 +1,25 @@
+/*******************************************************************************
+ * 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;
+
+public interface IPropertyContentVisitor<T> {
+
+	T visit(TextualContent content);
+
+	T visit(AttributeDependendContent content);
+
+	T visit(ProcessingInstructionTargetContent content);
+
+	T visit(URIContent content);
+
+	T visit(ImageContent content);
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/ImageContent.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/ImageContent.java
new file mode 100644
index 0000000..2244400
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/ImageContent.java
@@ -0,0 +1,54 @@
+package org.eclipse.vex.core.internal.css;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.List;
+
+public class ImageContent implements IPropertyContent {
+
+	public final String baseURI;
+	public final List<IPropertyContent> parameters;
+
+	public ImageContent(final String baseURI, final List<IPropertyContent> parameters) {
+		this.baseURI = baseURI;
+		this.parameters = parameters;
+	}
+
+	@Override
+	public <T> T accept(final IPropertyContentVisitor<T> visitor) {
+		return visitor.visit(this);
+	}
+
+	public URL getResolvedImageURL() throws MalformedURLException {
+		final String urlSpecification = getParametersAsString();
+		final URL baseURL = getBaseURL();
+		if (baseURL == null) {
+			return new URL(urlSpecification);
+		} else {
+			return new URL(baseURL, urlSpecification);
+		}
+	}
+
+	private String getParametersAsString() {
+		final StringBuilder string = new StringBuilder();
+		for (final IPropertyContent parameter : parameters) {
+			string.append(parameter.toString());
+		}
+		return string.toString();
+	}
+
+	private URL getBaseURL() throws MalformedURLException {
+		final URL baseURL;
+		if (baseURI == null) {
+			baseURL = null;
+		} else {
+			baseURL = new URL(baseURI);
+		}
+		return baseURL;
+	}
+
+	@Override
+	public String toString() {
+		return "ImageContent [baseURI=" + baseURI + ", urlSpecification=" + getParametersAsString() + "]";
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/LengthProperty.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/LengthProperty.java
index 52f6874..fb39ae9 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/LengthProperty.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/LengthProperty.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2004, 2008 John Krasnay and others.
+ * Copyright (c) 2004, 2016 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
@@ -7,6 +7,7 @@
  *
  * Contributors:
  *     John Krasnay - initial API and implementation
+ *     Florian Thienel - add support for attribute dependend values
  *******************************************************************************/
 package org.eclipse.vex.core.internal.css;
 
@@ -17,6 +18,7 @@
 import org.eclipse.core.runtime.Status;
 import org.eclipse.vex.core.internal.VEXCorePlugin;
 import org.eclipse.vex.core.internal.core.DisplayDevice;
+import org.eclipse.vex.core.internal.core.Length;
 import org.eclipse.vex.core.provisional.dom.BaseNodeVisitorWithResult;
 import org.eclipse.vex.core.provisional.dom.IElement;
 import org.eclipse.vex.core.provisional.dom.INode;
@@ -39,25 +41,38 @@
 
 	@Override
 	public Object calculate(final LexicalUnit lu, final Styles parentStyles, final Styles styles, final INode node) {
-		final int ppi = getPpi();
 		if (isAttr(lu)) {
-			return node.accept(new BaseNodeVisitorWithResult<Object>(RelativeLength.createAbsolute(0)) {
+			return node.accept(new BaseNodeVisitorWithResult<Object>(Length.absolute(0)) {
 				@Override
 				public Object visit(final IElement element) {
-					return calculate(parseAttribute(lu, element), parentStyles, styles, element);
+					return new Length() {
+						@Override
+						public int get(final int referenceLength) {
+							return parseLength(parseAttribute(lu, element), parentStyles, styles).get(referenceLength);
+						}
+
+						@Override
+						public int getBaseValue() {
+							return parseLength(parseAttribute(lu, element), parentStyles, styles).getBaseValue();
+						}
+					};
 				}
 			});
 		}
+		return parseLength(lu, parentStyles, styles);
+	}
+
+	private Length parseLength(final LexicalUnit lu, final Styles parentStyles, final Styles styles) {
 		if (isLength(lu)) {
-			final int length = getIntLength(lu, styles.getFontSize(), ppi);
-			return RelativeLength.createAbsolute(length);
+			final int length = getIntLength(lu, styles.getFontSize(), getPpi());
+			return Length.absolute(length);
 		} else if (isPercentage(lu)) {
-			return RelativeLength.createRelative(lu.getFloatValue() / 100);
+			return Length.relative(lu.getFloatValue() / 100);
 		} else if (isInherit(lu) && parentStyles != null) {
-			return parentStyles.get(getName());
+			return (Length) parentStyles.get(getName());
 		} else {
 			// not specified, "auto", or other unknown value
-			return RelativeLength.createAbsolute(0);
+			return Length.ZERO;
 		}
 	}
 
@@ -85,7 +100,10 @@
 
 	private int getPpi() {
 		final DisplayDevice device = DisplayDevice.getCurrent();
-		final int ppi = axis == Axis.HORIZONTAL ? device.getHorizontalPPI() : device.getVerticalPPI();
-		return ppi;
+		if (axis == Axis.HORIZONTAL) {
+			return device.getHorizontalPPI();
+		} else {
+			return device.getVerticalPPI();
+		}
 	}
 }
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/LineHeightProperty.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/LineHeightProperty.java
index 7041482..a035202 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/LineHeightProperty.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/LineHeightProperty.java
@@ -11,6 +11,7 @@
 package org.eclipse.vex.core.internal.css;
 
 import org.eclipse.vex.core.internal.core.DisplayDevice;
+import org.eclipse.vex.core.internal.core.Length;
 import org.eclipse.vex.core.provisional.dom.INode;
 import org.w3c.css.sac.LexicalUnit;
 
@@ -38,23 +39,23 @@
 		final int ppi = DisplayDevice.getCurrent().getVerticalPPI();
 
 		if (isLength(lu)) {
-			return RelativeLength.createAbsolute(Math.round(getIntLength(lu, styles.getFontSize(), ppi) / styles.getFontSize()));
+			return Length.absolute(Math.round(getIntLength(lu, styles.getFontSize(), ppi) / styles.getFontSize()));
 		} else if (isNumber(lu)) {
 			if (getNumber(lu) <= 0) {
-				return RelativeLength.createRelative(LINE_HEIGHT_NORMAL);
+				return Length.relative(LINE_HEIGHT_NORMAL);
 			} else {
-				return RelativeLength.createRelative(getNumber(lu));
+				return Length.relative(getNumber(lu));
 			}
 		} else if (isPercentage(lu)) {
 			if (lu.getFloatValue() <= 0) {
-				return RelativeLength.createRelative(LINE_HEIGHT_NORMAL);
+				return Length.relative(LINE_HEIGHT_NORMAL);
 			} else {
-				return RelativeLength.createRelative(lu.getFloatValue() / 100);
+				return Length.relative(lu.getFloatValue() / 100);
 			}
 		} else {
 			// not specified, "inherit", or other unknown value
 			if (parentStyles == null) {
-				return RelativeLength.createRelative(LINE_HEIGHT_NORMAL);
+				return Length.relative(LINE_HEIGHT_NORMAL);
 			} else {
 				return parentStyles.get(CSS.LINE_HEIGHT);
 			}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/ListStyleTypeProperty.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/ListStyleTypeProperty.java
index 3ff6c9b..03ff24d 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/ListStyleTypeProperty.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/ListStyleTypeProperty.java
@@ -27,17 +27,11 @@
 		if (isListStyleType(lu)) {
 			return lu.getStringValue();
 		} else {
-			if (parentStyles == null) {
-				return CSS.DISC;
-			} else {
-				return parentStyles.getListStyleType();
-			}
+			return CSS.NONE;
 		}
-
 	}
 
 	private static boolean isListStyleType(final LexicalUnit lu) {
-
 		if (lu == null || lu.getLexicalUnitType() != LexicalUnit.SAC_IDENT) {
 			return false;
 		}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/ProcessingInstructionTargetContent.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/ProcessingInstructionTargetContent.java
new file mode 100644
index 0000000..5d760f1
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/ProcessingInstructionTargetContent.java
@@ -0,0 +1,33 @@
+/*******************************************************************************
+ * 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 org.eclipse.vex.core.provisional.dom.IProcessingInstruction;
+
+public class ProcessingInstructionTargetContent implements IPropertyContent {
+
+	public final IProcessingInstruction processingInstruction;
+
+	public ProcessingInstructionTargetContent(final IProcessingInstruction processingInstruction) {
+		this.processingInstruction = processingInstruction;
+	}
+
+	@Override
+	public <T> T accept(final IPropertyContentVisitor<T> visitor) {
+		return visitor.visit(this);
+	}
+
+	@Override
+	public String toString() {
+		return processingInstruction.getTarget();
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/RelativeLength.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/RelativeLength.java
deleted file mode 100644
index 64d16ca..0000000
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/RelativeLength.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2004, 2008 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
- * http://www.eclipse.org/legal/epl-v10.html
- *
- * Contributors:
- *     John Krasnay - initial API and implementation
- *******************************************************************************/
-package org.eclipse.vex.core.internal.css;
-
-/**
- * A length that may be expressed as an absolute or relative value.
- */
-public class RelativeLength {
-
-	private final float percentage;
-	private final int absolute;
-	boolean isAbsolute;
-
-	private static RelativeLength ZERO = new RelativeLength(0, 0, true);
-
-	/**
-	 * Create a relative length representing an absolute value.
-	 *
-	 * @return the new RelativeLength value.
-	 */
-	public static RelativeLength createAbsolute(final int value) {
-		if (value == 0) {
-			return ZERO;
-		} else {
-			return new RelativeLength(0, value, true);
-		}
-	}
-
-	/**
-	 * Create a relative length representing a relative value.
-	 *
-	 * @return the new RelativeLength value.
-	 */
-	public static RelativeLength createRelative(final float percentage) {
-		return new RelativeLength(percentage, 0, false);
-	}
-
-	/**
-	 * Return the value of the length given a reference value. If this object represents an absolute value, that value
-	 * is simply returned. Otherwise, returns the given reference length multiplied by the given percentage and rounded
-	 * to the nearest integer.
-	 *
-	 * @param referenceLength
-	 *            reference length by which percentage lengths will by multiplied.
-	 * @return the actual value
-	 */
-	public int get(final int referenceLength) {
-		if (isAbsolute) {
-			return absolute;
-		} else {
-			return Math.round(percentage * referenceLength);
-		}
-	}
-
-	// ==================================================== PRIVATE
-
-	private RelativeLength(final float percentage, final int absolute, final boolean isAbsolute) {
-		this.percentage = percentage;
-		this.absolute = absolute;
-		this.isAbsolute = isAbsolute;
-	}
-}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/RomanNumeral.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/RomanNumeral.java
new file mode 100644
index 0000000..da157db
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/RomanNumeral.java
@@ -0,0 +1,82 @@
+/*******************************************************************************
+ * 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;
+
+public class RomanNumeral {
+
+	private final int value;
+	private final String numeral;
+
+	public RomanNumeral(final int value) {
+		this.value = value;
+		numeral = toRoman(value);
+	}
+
+	private static String toRoman(final int value) {
+		for (final Digit digit : Digit.values()) {
+			if (value >= digit.value) {
+				return digit.toString() + toRoman(value - digit.value);
+			}
+		}
+		return "";
+	}
+
+	public int intValue() {
+		return value;
+	}
+
+	@Override
+	public String toString() {
+		return numeral;
+	}
+
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = 1;
+		result = prime * result + (numeral == null ? 0 : numeral.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 RomanNumeral other = (RomanNumeral) obj;
+		if (numeral == null) {
+			if (other.numeral != null) {
+				return false;
+			}
+		} else if (!numeral.equals(other.numeral)) {
+			return false;
+		}
+		return true;
+	}
+
+	private static enum Digit {
+
+		M(1000), CM(900), D(500), CD(400), C(100), XC(90), L(50), XL(40), X(10), IX(9), V(5), IV(4), I(1);
+
+		public final int value;
+
+		private Digit(final int value) {
+			this.value = value;
+		}
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/StyleSheet.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/StyleSheet.java
index 719823b..568db74 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/StyleSheet.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/StyleSheet.java
@@ -23,6 +23,8 @@
 package org.eclipse.vex.core.internal.css;
 
 import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -49,7 +51,7 @@
  */
 public class StyleSheet {
 
-	public static final StyleSheet NULL = new StyleSheet(Collections.<Rule> emptyList());
+	public static final StyleSheet NULL = new StyleSheet(Collections.<Rule> emptyList(), null);
 
 	private static final Comparator<PropertyDecl> PROPERTY_CASCADE_ORDERING = new Comparator<PropertyDecl>() {
 		@Override
@@ -84,12 +86,12 @@
 			new BorderWidthProperty(CSS.BORDER_LEFT_WIDTH, CSS.BORDER_LEFT_STYLE, IProperty.Axis.HORIZONTAL),
 			new BorderWidthProperty(CSS.BORDER_RIGHT_WIDTH, CSS.BORDER_RIGHT_STYLE, IProperty.Axis.HORIZONTAL),
 			new BorderWidthProperty(CSS.BORDER_TOP_WIDTH, CSS.BORDER_TOP_STYLE, IProperty.Axis.VERTICAL), new BorderSpacingProperty(), new LengthProperty(CSS.HEIGHT, IProperty.Axis.VERTICAL),
-			new LengthProperty(CSS.WIDTH, IProperty.Axis.HORIZONTAL), new BackgroundImageProperty(), new OutlineContentProperty(), new InlineMarkerProperty() };
+			new LengthProperty(CSS.WIDTH, IProperty.Axis.HORIZONTAL), new BackgroundImageProperty(), new OutlineContentProperty(), new InlineMarkerProperty(),
+			new ContentProperty()
+	};
 
-	/**
-	 * The rules that comprise the stylesheet.
-	 */
 	private final List<Rule> rules;
+	private final URL baseUrl;
 
 	/**
 	 * The VEX core styles
@@ -122,8 +124,26 @@
 	 * @param rules
 	 *            Rules that constitute the style sheet.
 	 */
-	public StyleSheet(final Collection<Rule> rules) {
+	public StyleSheet(final Collection<Rule> rules, final URL baseUrl) {
 		this.rules = new ArrayList<Rule>(rules);
+		this.baseUrl = baseUrl;
+	}
+
+	public URL getBaseUrl() {
+		return baseUrl;
+	}
+
+	public URL resolveUrl(final String urlSpecification) {
+		try {
+			if (baseUrl == null) {
+				return new URL(urlSpecification);
+			} else {
+				return new URL(baseUrl, urlSpecification);
+			}
+		} catch (final MalformedURLException e) {
+			e.printStackTrace(); // TODO log
+			return null;
+		}
 	}
 
 	/**
@@ -163,6 +183,7 @@
 	 * @return the 'before' pseudo-element for the given parent element, or null if there is no such element defined in
 	 *         the stylesheet
 	 */
+	@Deprecated
 	public IElement getPseudoElementBefore(final INode parent) {
 		return getPseudoElement(parent, CSS.PSEUDO_BEFORE);
 	}
@@ -173,6 +194,7 @@
 	 * @return the 'after' pseudo-element for the given parent element, or null if there is no such element defined in
 	 *         the stylesheet
 	 */
+	@Deprecated
 	public IElement getPseudoElementAfter(final INode parent) {
 		return getPseudoElement(parent, CSS.PSEUDO_AFTER);
 	}
@@ -180,18 +202,18 @@
 	private IElement getPseudoElement(final INode parent, final String pseudoElementName) {
 		Assert.isNotNull(parent, "The parent node must not be null!");
 
-		final String name = pseudoElementName.toLowerCase();
+		final org.eclipse.vex.core.internal.css.Styles.PseudoElement pseudoElement = org.eclipse.vex.core.internal.css.Styles.PseudoElement.parse(pseudoElementName);
 		final Styles parentStyles = getStyles(parent);
-		if (parentStyles == null || !parentStyles.hasPseudoElement(name)) {
+		if (parentStyles == null || !parentStyles.hasPseudoElement(pseudoElement)) {
 			return null;
 		}
 
-		final Styles pseudoElementStyles = parentStyles.getPseudoElementStyles(name);
+		final Styles pseudoElementStyles = parentStyles.getPseudoElementStyles(pseudoElement);
 		if (pseudoElementStyles == null || !pseudoElementStyles.isContentDefined()) {
 			return null;
 		}
 
-		return new PseudoElement(parent, name);
+		return new PseudoElement(parent, pseudoElementName.toLowerCase());
 	}
 
 	/**
@@ -203,7 +225,7 @@
 	public Styles getStyles(final INode node) {
 
 		if (node instanceof PseudoElement) {
-			return getStyles(((PseudoElement) node).getParentNode()).getPseudoElementStyles(((PseudoElement) node).getName());
+			return getStyles(((PseudoElement) node).getParentNode()).getPseudoElementStyles(org.eclipse.vex.core.internal.css.Styles.PseudoElement.parse(((PseudoElement) node).getName()));
 		} else {
 			if (styleMap.containsKey(node)) {
 				return styleMap.get(node);
@@ -237,7 +259,7 @@
 		for (final Entry<String, Map<String, LexicalUnit>> entry : decls.entrySet()) {
 			final String pseudoElement = entry.getKey();
 			final Styles pseudoElementStyles = calculateNodeStyles(node, entry.getValue(), styles);
-			styles.putPseudoElementStyles(pseudoElement, pseudoElementStyles);
+			styles.putPseudoElementStyles(org.eclipse.vex.core.internal.css.Styles.PseudoElement.parse(pseudoElement), pseudoElementStyles);
 		}
 
 		return styles;
@@ -246,28 +268,10 @@
 	private Styles calculateNodeStyles(final INode node, final Map<String, LexicalUnit> decls, final Styles parentStyles) {
 		final Styles styles = new Styles();
 
-		LexicalUnit lexicalUnit;
-		lexicalUnit = decls.get(CSS.CONTENT);
-		// Content needs special handling, since the value of attr(xxx) may change while editing
-		// We pass all valid LexicalUnits to Styles and evaluate there on every access
-		final List<LexicalUnit> content = new ArrayList<LexicalUnit>();
-		while (lexicalUnit != null) {
-			switch (lexicalUnit.getLexicalUnitType()) {
-			case LexicalUnit.SAC_STRING_VALUE:
-				// content: "A String"
-				content.add(lexicalUnit);
-				break;
-			case LexicalUnit.SAC_ATTR:
-				// content: attr(attributeName)
-				content.add(lexicalUnit);
-				break;
-			}
-			lexicalUnit = lexicalUnit.getNextLexicalUnit();
-		}
-		styles.setContent(content);
+		styles.setBaseUrl(baseUrl);
 
 		for (final IProperty property : CSS_PROPERTIES) {
-			lexicalUnit = decls.get(property.getName());
+			final LexicalUnit lexicalUnit = decls.get(property.getName());
 			final Object value = property.calculate(lexicalUnit, parentStyles, styles, node);
 			styles.put(property.getName(), value);
 		}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/StyleSheetReader.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/StyleSheetReader.java
index f528d5d..520a169 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/StyleSheetReader.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/StyleSheetReader.java
@@ -175,7 +175,7 @@
 	 */
 	public StyleSheet read(final InputSource inputSource, final URL url) throws CSSException, IOException {
 		final List<Rule> rules = readRules(inputSource, url);
-		return new StyleSheet(rules);
+		return new StyleSheet(rules, url);
 	}
 
 	/**
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/Styles.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/Styles.java
index 3af931a..d609345 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/Styles.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/Styles.java
@@ -13,24 +13,42 @@
  *******************************************************************************/
 package org.eclipse.vex.core.internal.css;
 
-import java.util.ArrayList;
+import java.net.MalformedURLException;
+import java.net.URL;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
 import org.eclipse.vex.core.internal.core.Color;
 import org.eclipse.vex.core.internal.core.FontSpec;
-import org.eclipse.vex.core.provisional.dom.BaseNodeVisitor;
-import org.eclipse.vex.core.provisional.dom.IElement;
-import org.eclipse.vex.core.provisional.dom.INode;
-import org.eclipse.vex.core.provisional.dom.IProcessingInstruction;
-import org.w3c.css.sac.LexicalUnit;
+import org.eclipse.vex.core.internal.core.Length;
 
 /**
  * Represents the computed style properties for a particular element.
  */
 public class Styles {
 
+	public static enum PseudoElement {
+		BEFORE, AFTER;
+
+		public static PseudoElement parse(final String string) {
+			if (string == null) {
+				throw new NullPointerException("Cannot parse 'null' as PseudoElement.");
+			}
+			final String lowerString = string.toLowerCase();
+			for (final PseudoElement value : values()) {
+				if (value.toString().toLowerCase().equals(lowerString)) {
+					return value;
+				}
+			}
+			throw new IllegalArgumentException("'" + string + "' is not a valid PseudoElement name.");
+		}
+
+		public String key() {
+			return toString().toLowerCase();
+		}
+	}
+
 	/** Maps property name (String) => value (Object) */
 	private final Map<String, Object> values = new HashMap<String, Object>();
 
@@ -40,8 +58,29 @@
 	 */
 	private final Map<String, Styles> pseudoElementStyles = new HashMap<String, Styles>();
 
-	private List<LexicalUnit> contentLexicalUnits;
 	private FontSpec font;
+	private URL baseUrl;
+
+	public URL getBaseUrl() {
+		return baseUrl;
+	}
+
+	public void setBaseUrl(final URL baseUrl) {
+		this.baseUrl = baseUrl;
+	}
+
+	public URL resolveUrl(final String urlSpecification) {
+		try {
+			if (baseUrl == null) {
+				return new URL(urlSpecification);
+			} else {
+				return new URL(baseUrl, urlSpecification);
+			}
+		} catch (final MalformedURLException e) {
+			e.printStackTrace(); // TODO log
+			return null;
+		}
+	}
 
 	/**
 	 * Returns the value of the given property, or null if the property does not have a value.
@@ -134,7 +173,10 @@
 	 * @return <code>true</code> if the stylesheet defined content for this element.
 	 */
 	public boolean isContentDefined() {
-		return contentLexicalUnits.size() > 0;
+		if (!values.containsKey(CSS.CONTENT)) {
+			return false;
+		}
+		return !getContent().isEmpty();
 	}
 
 	/**
@@ -146,38 +188,17 @@
 	 * @param node
 	 *            The INode to get attr(...) values from
 	 */
-	public List<String> getContent(final INode node) {
-		final List<String> content = new ArrayList<String>();
-		for (LexicalUnit lexicalUnit : contentLexicalUnits) {
-			switch (lexicalUnit.getLexicalUnitType()) {
-			case LexicalUnit.SAC_STRING_VALUE:
-				// content: "A String"
-				content.add(lexicalUnit.getStringValue());
-				break;
-			case LexicalUnit.SAC_ATTR:
-				// content: attr(attributeName)
-				final LexicalUnit currentLexicalUnit = lexicalUnit;
-				node.accept(new BaseNodeVisitor() {
-					@Override
-					public void visit(final IElement element) {
-						final String attributeValue = element.getAttributeValue(currentLexicalUnit.getStringValue());
-						if (attributeValue != null) {
-							content.add(attributeValue);
-						}
-					}
-
-					@Override
-					public void visit(final IProcessingInstruction pi) {
-						if (currentLexicalUnit.getStringValue().equalsIgnoreCase(CSS.PSEUDO_TARGET)) {
-							content.add(pi.getTarget());
-						}
-					}
-				});
-				break;
-			}
-			lexicalUnit = lexicalUnit.getNextLexicalUnit();
+	public String getTextualContent() {
+		final StringBuilder string = new StringBuilder();
+		for (final IPropertyContent content : getContent()) {
+			string.append(content.toString());
 		}
-		return content;
+		return string.toString();
+	}
+
+	@SuppressWarnings("unchecked")
+	public List<IPropertyContent> getContent() {
+		return (List<IPropertyContent>) values.get(CSS.CONTENT);
 	}
 
 	/**
@@ -240,7 +261,7 @@
 	 * Returns the value of the <code>lineHeight</code> property.
 	 */
 	public int getLineHeight() {
-		return ((RelativeLength) values.get(CSS.LINE_HEIGHT)).get(Math.round(getFontSize()));
+		return ((Length) values.get(CSS.LINE_HEIGHT)).get(Math.round(getFontSize()));
 	}
 
 	/**
@@ -283,15 +304,15 @@
 		values.put(propertyName, value);
 	}
 
-	public void putPseudoElementStyles(final String pseudoElementName, final Styles pseudoElStyles) {
-		pseudoElementStyles.put(pseudoElementName, pseudoElStyles);
+	public void putPseudoElementStyles(final PseudoElement pseudoElement, final Styles styles) {
+		pseudoElementStyles.put(pseudoElement.key(), styles);
 	}
 
-	public Styles getPseudoElementStyles(final String pseudoElementName) {
-		if (pseudoElementStyles.containsKey(pseudoElementName.toLowerCase())) {
-			return pseudoElementStyles.get(pseudoElementName.toLowerCase());
+	public Styles getPseudoElementStyles(final PseudoElement pseudoElement) {
+		if (hasPseudoElement(pseudoElement)) {
+			return pseudoElementStyles.get(pseudoElement.key());
 		} else {
-			// There are no styles for the given pseudo element - return this
+			// There are no styles for the given pseudo element - return this; better save than sorry!
 			return this;
 		}
 	}
@@ -299,21 +320,11 @@
 	/**
 	 * Check if the given pseudo element is defined for this node.
 	 *
-	 * @param pseudoElementName
+	 * @param pseudoElement
 	 * @return <code>true</code> when the given pseudo element is defined.
 	 */
-	public boolean hasPseudoElement(final String pseudoElementName) {
-		return pseudoElementStyles.containsKey(pseudoElementName.toLowerCase());
-	}
-
-	/**
-	 * Sets the LexicalUnits of the <code>content</code> property.
-	 *
-	 * @param content
-	 *            <code>List</code> of <code>LexicalUnits</code> objects defining the content.
-	 */
-	public void setContent(final List<LexicalUnit> content) {
-		contentLexicalUnits = content;
+	public boolean hasPseudoElement(final PseudoElement pseudoElement) {
+		return pseudoElementStyles.containsKey(pseudoElement.key());
 	}
 
 	/**
@@ -326,12 +337,12 @@
 		this.font = font;
 	}
 
-	public RelativeLength getElementWidth() {
-		return (RelativeLength) values.get(CSS.WIDTH);
+	public Length getElementWidth() {
+		return (Length) values.get(CSS.WIDTH);
 	}
 
-	public RelativeLength getElementHeight() {
-		return (RelativeLength) values.get(CSS.HEIGHT);
+	public Length getElementHeight() {
+		return (Length) values.get(CSS.HEIGHT);
 	}
 
 	public boolean hasBackgroundImage() {
@@ -377,58 +388,58 @@
 	/**
 	 * @return the value of margin-bottom
 	 */
-	public RelativeLength getMarginBottom() {
-		return (RelativeLength) values.get(CSS.MARGIN_BOTTOM);
+	public Length getMarginBottom() {
+		return (Length) values.get(CSS.MARGIN_BOTTOM);
 		// return marginBottom;
 	}
 
 	/**
 	 * @return the value of margin-left
 	 */
-	public RelativeLength getMarginLeft() {
-		return (RelativeLength) values.get(CSS.MARGIN_LEFT);
+	public Length getMarginLeft() {
+		return (Length) values.get(CSS.MARGIN_LEFT);
 	}
 
 	/**
 	 * @return the value of margin-right
 	 */
-	public RelativeLength getMarginRight() {
-		return (RelativeLength) values.get(CSS.MARGIN_RIGHT);
+	public Length getMarginRight() {
+		return (Length) values.get(CSS.MARGIN_RIGHT);
 	}
 
 	/**
 	 * @return the value of margin-top
 	 */
-	public RelativeLength getMarginTop() {
-		return (RelativeLength) values.get(CSS.MARGIN_TOP);
+	public Length getMarginTop() {
+		return (Length) values.get(CSS.MARGIN_TOP);
 	}
 
 	/**
 	 * @return the value of padding-bottom
 	 */
-	public RelativeLength getPaddingBottom() {
-		return (RelativeLength) values.get(CSS.PADDING_BOTTOM);
+	public Length getPaddingBottom() {
+		return (Length) values.get(CSS.PADDING_BOTTOM);
 	}
 
 	/**
 	 * @return the value of padding-left
 	 */
-	public RelativeLength getPaddingLeft() {
-		return (RelativeLength) values.get(CSS.PADDING_LEFT);
+	public Length getPaddingLeft() {
+		return (Length) values.get(CSS.PADDING_LEFT);
 	}
 
 	/**
 	 * @return the value of padding-right
 	 */
-	public RelativeLength getPaddingRight() {
-		return (RelativeLength) values.get(CSS.PADDING_RIGHT);
+	public Length getPaddingRight() {
+		return (Length) values.get(CSS.PADDING_RIGHT);
 	}
 
 	/**
 	 * @return the value of padding-top
 	 */
-	public RelativeLength getPaddingTop() {
-		return (RelativeLength) values.get(CSS.PADDING_TOP);
+	public Length getPaddingTop() {
+		return (Length) values.get(CSS.PADDING_TOP);
 	}
 
 	/**
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/TextualContent.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/TextualContent.java
new file mode 100644
index 0000000..f2764eb
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/TextualContent.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.css;
+
+public class TextualContent implements IPropertyContent {
+
+	public final String text;
+
+	public TextualContent(final String text) {
+		this.text = text;
+	}
+
+	@Override
+	public <T> T accept(final IPropertyContentVisitor<T> visitor) {
+		return visitor.visit(this);
+	}
+
+	@Override
+	public String toString() {
+		return text;
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/URIContent.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/URIContent.java
new file mode 100644
index 0000000..11ed460
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/css/URIContent.java
@@ -0,0 +1,37 @@
+/*******************************************************************************
+ * 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 java.net.URI;
+import java.net.URISyntaxException;
+
+public class URIContent implements IPropertyContent {
+
+	public final String uri;
+
+	public URIContent(final String uri) {
+		this.uri = uri;
+	}
+
+	@Override
+	public <T> T accept(final IPropertyContentVisitor<T> visitor) {
+		return visitor.visit(this);
+	}
+
+	@Override
+	public String toString() {
+		return uri.toString();
+	}
+
+	public URI uriValue() throws URISyntaxException {
+		return new URI(uri);
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/ContentTopology.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/ContentTopology.java
new file mode 100644
index 0000000..8585fc1
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/ContentTopology.java
@@ -0,0 +1,512 @@
+/*******************************************************************************
+ * 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.cursor;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.eclipse.vex.core.internal.boxes.BaseBoxVisitorWithResult;
+import org.eclipse.vex.core.internal.boxes.DepthFirstBoxTraversal;
+import org.eclipse.vex.core.internal.boxes.IBox;
+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.ParentTraversal;
+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.provisional.dom.ContentRange;
+import org.eclipse.vex.core.provisional.dom.INode;
+
+/**
+ * @author Florian Thienel
+ */
+public class ContentTopology {
+
+	private RootBox rootBox;
+	private IContentBox outmostContentBox;
+
+	public void setRootBox(final RootBox rootBox) {
+		this.rootBox = rootBox;
+		outmostContentBox = findOutmostContentBox(rootBox);
+	}
+
+	private static IContentBox findOutmostContentBox(final RootBox rootBox) {
+		return rootBox.accept(new DepthFirstBoxTraversal<IContentBox>() {
+			@Override
+			public IContentBox visit(final StructuralNodeReference box) {
+				return box;
+			}
+
+			@Override
+			public IContentBox visit(final InlineNodeReference box) {
+				return box;
+			}
+
+			@Override
+			public IContentBox visit(final TextContent box) {
+				return box;
+			}
+
+			@Override
+			public IContentBox visit(final NodeEndOffsetPlaceholder box) {
+				return box;
+			}
+		});
+	}
+
+	public int getLastOffset() {
+		if (outmostContentBox == null) {
+			return 0;
+		}
+		return outmostContentBox.getEndOffset();
+	}
+
+	public IContentBox getOutmostContentBox() {
+		return outmostContentBox;
+	}
+
+	public IContentBox findBoxForPosition(final int offset) {
+		return findBoxForPosition(offset, rootBox);
+	}
+
+	public IContentBox findBoxForPosition(final int offset, final IBox startBox) {
+		if (startBox == null) {
+			if (rootBox == null) {
+				return null;
+			}
+			return findBoxForPosition(offset, rootBox);
+		}
+
+		return startBox.accept(new DepthFirstBoxTraversal<IContentBox>() {
+			@Override
+			public IContentBox visit(final StructuralNodeReference box) {
+				if (box.getStartOffset() == offset || box.getEndOffset() == offset) {
+					final IContentBox childBox = box.getComponent().accept(this);
+					if (childBox != null) {
+						return childBox;
+					}
+					return box;
+				}
+				if (box.getStartOffset() < offset && box.getEndOffset() > offset) {
+					return box.getComponent().accept(this);
+				}
+				return null;
+			}
+
+			@Override
+			public IContentBox visit(final InlineNodeReference box) {
+				if (box.getStartOffset() == offset || box.getEndOffset() == offset) {
+					final IContentBox childBox = box.getComponent().accept(this);
+					if (childBox != null) {
+						return childBox;
+					}
+					return box;
+				}
+				if (box.getStartOffset() < offset && box.getEndOffset() > offset) {
+					return box.getComponent().accept(this);
+				}
+				return null;
+			}
+
+			@Override
+			public IContentBox visit(final TextContent box) {
+				if (box.getStartOffset() <= offset && box.getEndOffset() >= offset) {
+					return box;
+				}
+				return null;
+			}
+
+			@Override
+			public IContentBox visit(final NodeEndOffsetPlaceholder box) {
+				if (box.getStartOffset() <= offset && box.getEndOffset() >= offset) {
+					return box;
+				}
+				return null;
+			}
+		});
+	}
+
+	public IContentBox findBoxForRange(final ContentRange range) {
+		return rootBox.accept(new DepthFirstBoxTraversal<IContentBox>() {
+			@Override
+			public IContentBox visit(final StructuralNodeReference box) {
+				if (box.getRange().contains(range)) {
+					final IContentBox childBox = box.getComponent().accept(this);
+					if (childBox == null) {
+						return box;
+					} else {
+						return childBox;
+					}
+				}
+
+				return null;
+			}
+
+			@Override
+			public IContentBox visit(final InlineNodeReference box) {
+				if (box.getRange().contains(range)) {
+					final IContentBox childBox = box.getComponent().accept(this);
+					if (childBox == null) {
+						return box;
+					} else {
+						return childBox;
+					}
+				}
+				return null;
+			}
+
+			@Override
+			public IContentBox visit(final TextContent box) {
+				if (box.getRange().contains(range)) {
+					return box;
+				}
+				return null;
+			}
+
+			@Override
+			public IContentBox visit(final NodeEndOffsetPlaceholder box) {
+				if (box.getRange().contains(range)) {
+					return box;
+				}
+				return null;
+			}
+		});
+	}
+
+	public IContentBox findBoxForCoordinates(final int x, final int y) {
+		if (outmostContentBox == null) {
+			return null;
+		}
+
+		return outmostContentBox.accept(new DepthFirstBoxTraversal<IContentBox>() {
+			@Override
+			public IContentBox visit(final StructuralNodeReference box) {
+				if (!box.containsCoordinates(x, y)) {
+					return null;
+				}
+				final IContentBox deeperContainer = super.visit(box);
+				if (deeperContainer != null) {
+					return deeperContainer;
+				}
+				return box;
+			}
+
+			@Override
+			public IContentBox visit(final InlineNodeReference box) {
+				if (!box.containsCoordinates(x, y)) {
+					return null;
+				}
+				final IContentBox deeperContainer = super.visit(box);
+				if (deeperContainer != null) {
+					return deeperContainer;
+				}
+				return box;
+			}
+
+			@Override
+			public IContentBox visit(final TextContent box) {
+				if (!box.containsCoordinates(x, y)) {
+					return null;
+				}
+				return box;
+			}
+
+			@Override
+			public IContentBox visit(final NodeEndOffsetPlaceholder box) {
+				if (!box.containsCoordinates(x, y)) {
+					return null;
+				}
+				return box;
+			}
+		});
+	}
+
+	public Collection<IContentBox> findBoxesForNode(final INode node) {
+		return rootBox.accept(new DepthFirstBoxTraversal<Collection<IContentBox>>() {
+			private final LinkedList<IContentBox> boxesForNode = new LinkedList<IContentBox>();
+
+			@Override
+			public Collection<IContentBox> visit(final RootBox box) {
+				super.visit(box);
+				return boxesForNode;
+			}
+
+			@Override
+			public Collection<IContentBox> visit(final StructuralNodeReference box) {
+				if (node == box.getNode()) {
+					boxesForNode.add(box);
+					return null;
+				}
+				if (box.getStartOffset() > node.getEndOffset()) {
+					return boxesForNode;
+				}
+				if (!box.getRange().intersects(node.getRange())) {
+					return null;
+				}
+				super.visit(box);
+				return null;
+			}
+
+			@Override
+			public Collection<IContentBox> visit(final InlineNodeReference box) {
+				if (node == box.getNode()) {
+					boxesForNode.add(box);
+				}
+				if (box.getStartOffset() > node.getEndOffset()) {
+					return boxesForNode;
+				}
+				if (!box.getRange().intersects(node.getRange())) {
+					return null;
+				}
+				super.visit(box);
+				return null;
+			}
+		});
+	}
+
+	public IContentBox findClosestBoxByCoordinates(final int x, final int y) {
+		final IContentBox deepestContainer = findBoxForCoordinates(x, y);
+		if (deepestContainer == null) {
+			return null;
+		}
+		return findClosestBoxInContainer(deepestContainer, x, y);
+	}
+
+	private static IContentBox findClosestBoxInContainer(final IContentBox container, final int x, final int y) {
+		final LinkedList<IContentBox> candidates = new LinkedList<IContentBox>();
+		container.accept(new DepthFirstBoxTraversal<Object>() {
+			@Override
+			public Object visit(final TextContent box) {
+				if (box.containsY(y)) {
+					candidates.add(box);
+				}
+				return null;
+			}
+
+			@Override
+			public Object visit(final NodeEndOffsetPlaceholder box) {
+				if (box.containsY(y)) {
+					candidates.add(box);
+				}
+				return null;
+			}
+		});
+
+		int minHorizontalDistance = Integer.MAX_VALUE;
+		IContentBox closestBox = container;
+		for (final IContentBox candidate : candidates) {
+			final int horizontalDistance = horizontalDistance(candidate, x);
+			if (horizontalDistance < minHorizontalDistance) {
+				minHorizontalDistance = horizontalDistance;
+				closestBox = candidate;
+			}
+		}
+
+		return closestBox;
+	}
+
+	public static IContentBox getParentContentBox(final IBox childBox) {
+		return childBox.accept(new ParentTraversal<IContentBox>() {
+			@Override
+			public IContentBox visit(final StructuralNodeReference box) {
+				if (box == childBox) {
+					return super.visit(box);
+				}
+				return box;
+			}
+
+			@Override
+			public IContentBox visit(final InlineNodeReference box) {
+				if (box == childBox) {
+					return super.visit(box);
+				}
+				return box;
+			}
+		});
+	}
+
+	public static IContentBox findClosestContentBoxChildBelow(final IContentBox parent, final int x, final int y) {
+		final Iterable<IContentBox> candidates = findVerticallyClosestContentBoxChildrenBelow(parent, y);
+		return findHorizontallyClosestContentBox(candidates, x);
+	}
+
+	public static Iterable<IContentBox> findVerticallyClosestContentBoxChildrenBelow(final IContentBox parent, final int y) {
+		final LinkedList<IContentBox> candidates = new LinkedList<IContentBox>();
+		final int[] minVerticalDistance = new int[1];
+		minVerticalDistance[0] = Integer.MAX_VALUE;
+		parent.accept(new DepthFirstBoxTraversal<Object>() {
+			@Override
+			public Object visit(final StructuralNodeReference box) {
+				if (box == parent) {
+					super.visit(box);
+				} else {
+					final int distance = verticalDistance(box, y);
+					if (box.isBelow(y)) {
+						candidates.add(box);
+						minVerticalDistance[0] = Math.min(distance, minVerticalDistance[0]);
+					}
+				}
+				return null;
+			}
+
+			@Override
+			public Object visit(final InlineNodeReference box) {
+				if (box == parent) {
+					super.visit(box);
+				} else {
+					final int distance = verticalDistance(box, y);
+					if (box.isBelow(y)) {
+						candidates.add(box);
+						minVerticalDistance[0] = Math.min(distance, minVerticalDistance[0]);
+					}
+				}
+				return null;
+			}
+
+			@Override
+			public Object visit(final TextContent box) {
+				final int distance = verticalDistance(box, y);
+				if (box.isBelow(y)) {
+					candidates.add(box);
+					minVerticalDistance[0] = Math.min(distance, minVerticalDistance[0]);
+				}
+				return null;
+			}
+
+			@Override
+			public Object visit(final NodeEndOffsetPlaceholder box) {
+				final int distance = verticalDistance(box, y);
+				if (box.isBelow(y)) {
+					candidates.add(box);
+					minVerticalDistance[0] = Math.min(distance, minVerticalDistance[0]);
+				}
+				return null;
+			}
+		});
+
+		removeVerticallyDistantBoxes(candidates, y, minVerticalDistance[0]);
+		return candidates;
+	}
+
+	public static IContentBox findClosestContentBoxChildAbove(final IContentBox parent, final int x, final int y) {
+		final Iterable<IContentBox> candidates = findVerticallyClosestContentBoxChildrenAbove(parent, y);
+		return findHorizontallyClosestContentBox(candidates, x);
+	}
+
+	public static Iterable<IContentBox> findVerticallyClosestContentBoxChildrenAbove(final IContentBox parent, final int y) {
+		final LinkedList<IContentBox> candidates = new LinkedList<IContentBox>();
+		final int[] minVerticalDistance = new int[1];
+		minVerticalDistance[0] = Integer.MAX_VALUE;
+		parent.accept(new DepthFirstBoxTraversal<Object>() {
+			@Override
+			public Object visit(final StructuralNodeReference box) {
+				final int distance = verticalDistance(box, y);
+				if (box != parent && !box.isAbove(y)) {
+					return box;
+				}
+
+				if (box == parent) {
+					super.visit(box);
+				}
+
+				if (box != parent) {
+					candidates.add(box);
+					minVerticalDistance[0] = Math.min(distance, minVerticalDistance[0]);
+				}
+
+				return null;
+			}
+
+			@Override
+			public Object visit(final TextContent box) {
+				final int distance = verticalDistance(box, y);
+				if (box.isAbove(y) && distance <= minVerticalDistance[0]) {
+					candidates.add(box);
+					minVerticalDistance[0] = Math.min(distance, minVerticalDistance[0]);
+				}
+				return null;
+			}
+
+			@Override
+			public Object visit(final NodeEndOffsetPlaceholder box) {
+				final int distance = verticalDistance(box, y);
+				if (box.isAbove(y) && distance <= minVerticalDistance[0]) {
+					candidates.add(box);
+					minVerticalDistance[0] = Math.min(distance, minVerticalDistance[0]);
+				}
+				return null;
+			}
+		});
+
+		removeVerticallyDistantBoxes(candidates, y, minVerticalDistance[0]);
+		return candidates;
+	}
+
+	public static void removeVerticallyDistantBoxes(final List<? extends IContentBox> boxes, final int y, final int minVerticalDistance) {
+		for (final Iterator<? extends IContentBox> iter = boxes.iterator(); iter.hasNext();) {
+			final IContentBox candidate = iter.next();
+			if (verticalDistance(candidate, y) > minVerticalDistance) {
+				iter.remove();
+			}
+		}
+	}
+
+	public static int verticalDistance(final IContentBox box, final int y) {
+		return box.accept(new BaseBoxVisitorWithResult<Integer>(0) {
+			@Override
+			public Integer visit(final StructuralNodeReference box) {
+				return Math.abs(y - box.getAbsoluteTop() - box.getHeight());
+			}
+
+			@Override
+			public Integer visit(final InlineNodeReference box) {
+				return Math.abs(y - box.getAbsoluteTop() - box.getBaseline());
+			}
+
+			@Override
+			public Integer visit(final TextContent box) {
+				return Math.abs(y - box.getAbsoluteTop() - box.getBaseline());
+			}
+
+			@Override
+			public Integer visit(final NodeEndOffsetPlaceholder box) {
+				return Math.abs(y - box.getAbsoluteTop() - box.getBaseline());
+			}
+		});
+	}
+
+	public static IContentBox findHorizontallyClosestContentBox(final Iterable<? extends IContentBox> candidates, final int x) {
+		IContentBox finalCandidate = null;
+		int minHorizontalDistance = Integer.MAX_VALUE;
+		for (final IContentBox candidate : candidates) {
+			final int distance = horizontalDistance(candidate, x);
+			if (distance < minHorizontalDistance) {
+				finalCandidate = candidate;
+				minHorizontalDistance = distance;
+			}
+		}
+		return finalCandidate;
+	}
+
+	public static int horizontalDistance(final IBox box, final int x) {
+		if (box.getAbsoluteLeft() > x) {
+			return box.getAbsoluteLeft() - x;
+		}
+		if (box.getAbsoluteLeft() + box.getWidth() < x) {
+			return x - box.getAbsoluteLeft() - box.getWidth();
+		}
+		return 0;
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/Cursor.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/Cursor.java
new file mode 100644
index 0000000..7507a08
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/Cursor.java
@@ -0,0 +1,706 @@
+/*******************************************************************************
+ * Copyright (c) 2014, 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 java.util.LinkedList;
+
+import org.eclipse.vex.core.internal.boxes.BaseBoxVisitorWithResult;
+import org.eclipse.vex.core.internal.boxes.DepthFirstBoxTraversal;
+import org.eclipse.vex.core.internal.boxes.GraphicalBullet;
+import org.eclipse.vex.core.internal.boxes.IBox;
+import org.eclipse.vex.core.internal.boxes.IContentBox;
+import org.eclipse.vex.core.internal.boxes.IInlineBox;
+import org.eclipse.vex.core.internal.boxes.Image;
+import org.eclipse.vex.core.internal.boxes.InlineNodeReference;
+import org.eclipse.vex.core.internal.boxes.NodeEndOffsetPlaceholder;
+import org.eclipse.vex.core.internal.boxes.NodeTag;
+import org.eclipse.vex.core.internal.boxes.RootBox;
+import org.eclipse.vex.core.internal.boxes.Square;
+import org.eclipse.vex.core.internal.boxes.StaticText;
+import org.eclipse.vex.core.internal.boxes.StructuralNodeReference;
+import org.eclipse.vex.core.internal.boxes.TextContent;
+import org.eclipse.vex.core.internal.core.Color;
+import org.eclipse.vex.core.internal.core.FontSpec;
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.NodeGraphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+import org.eclipse.vex.core.internal.widget.IViewPort;
+import org.eclipse.vex.core.provisional.dom.ContentRange;
+import org.eclipse.vex.core.provisional.dom.INode;
+
+/**
+ * @author Florian Thienel
+ */
+public class Cursor implements ICursor {
+
+	public static final int CARET_BUFFER = 30;
+	private static final Color CARET_FOREGROUND_COLOR = new Color(255, 255, 255);
+	private static final Color CARET_BACKGROUND_COLOR = new Color(0, 0, 0);
+	private static final Color SELECTION_FOREGROUND_COLOR = new Color(255, 255, 255);
+	private static final Color SELECTION_BACKGROUND_COLOR = new Color(0, 0, 255);
+
+	private final ContentTopology contentTopology = new ContentTopology();
+	private final IContentSelector selector;
+	private final IViewPort viewPort;
+
+	private final LinkedList<MoveWithSelection> moves = new LinkedList<MoveWithSelection>();
+
+	private int offset;
+	private Caret caret;
+	private IContentBox box;
+	private int preferredX;
+	private boolean preferX;
+
+	private final LinkedList<ICursorPositionListener> cursorPositionListeners = new LinkedList<ICursorPositionListener>();
+
+	public Cursor(final IContentSelector selector, final IViewPort viewPort) {
+		this.selector = selector;
+		this.viewPort = viewPort;
+	}
+
+	public void setRootBox(final RootBox rootBox) {
+		contentTopology.setRootBox(rootBox);
+	}
+
+	@Override
+	public int getOffset() {
+		return offset;
+	}
+
+	@Override
+	public boolean hasSelection() {
+		return selector.isActive();
+	}
+
+	@Override
+	public ContentRange getSelectedRange() {
+		if (!hasSelection()) {
+			return new ContentRange(offset, offset);
+		}
+		return selector.getRange();
+	}
+
+	public int getDeltaIntoVisibleArea(final int top, final int height) {
+		final Rectangle caretArea = getVisibleArea();
+		if (caretArea.getY() + caretArea.getHeight() > top + height) {
+			return caretArea.getY() + caretArea.getHeight() - top - height;
+		}
+		if (caretArea.getY() < top) {
+			return caretArea.getY() - top;
+		}
+		return 0;
+	}
+
+	private Rectangle getVisibleArea() {
+		if (caret == null) {
+			return Rectangle.NULL;
+		}
+		return caret.getVisibleArea();
+	}
+
+	public Rectangle getCaretArea() {
+		if (caret == null) {
+			return Rectangle.NULL;
+		}
+		return caret.getHotArea();
+	}
+
+	@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) {
+			try {
+				listener.positionChanged(offset);
+			} catch (final Throwable t) {
+				t.printStackTrace();
+				// TODO remove listener?
+			}
+		}
+	}
+
+	private void firePositionAboutToChange() {
+		for (final ICursorPositionListener listener : cursorPositionListeners) {
+			try {
+				listener.positionAboutToChange();
+			} catch (final Throwable t) {
+				t.printStackTrace();
+				// TODO remove listener?
+			}
+		}
+	}
+
+	@Override
+	public void move(final ICursorMove move) {
+		moves.add(new MoveWithSelection(move, false));
+		firePositionAboutToChange();
+	}
+
+	@Override
+	public void select(final ICursorMove move) {
+		moves.add(new MoveWithSelection(move, true));
+		firePositionAboutToChange();
+	}
+
+	public void reconcile(final Graphics graphics) {
+		preferX = true;
+		applyCaretForPosition(graphics, offset);
+	}
+
+	public void applyMoves(final Graphics graphics) {
+		for (MoveWithSelection move = moves.poll(); move != null; move = moves.poll()) {
+			final int oldOffset = offset;
+			offset = move.move.calculateNewOffset(graphics, viewPort, contentTopology, offset, box, getHotArea(), preferredX);
+			if (move.select) {
+				if (move.move.isAbsolute()) {
+					selector.setEndAbsoluteTo(offset);
+				} else {
+					selector.moveEndTo(offset);
+				}
+				offset = selector.getCaretOffset();
+			} else {
+				selector.setMark(offset);
+			}
+			preferX = move.move.preferX();
+			applyCaretForPosition(graphics, offset);
+
+			if (oldOffset != offset) {
+				firePositionChanged(offset);
+			}
+		}
+	}
+
+	private Rectangle getHotArea() {
+		if (caret == null) {
+			return Rectangle.NULL;
+		}
+		return caret.getHotArea();
+	}
+
+	public void paint(final Graphics graphics) {
+		applyCaretForPosition(graphics, offset);
+		if (caret == null) {
+			return;
+		}
+
+		paintSelection(graphics);
+		caret.paint(graphics);
+	}
+
+	private void applyCaretForPosition(final Graphics graphics, final int offset) {
+		box = contentTopology.findBoxForPosition(offset);
+		if (box == null) {
+			return;
+		}
+
+		caret = getCaretForBox(graphics, box, offset);
+		if (preferX) {
+			preferredX = caret.getHotArea().getX();
+		}
+	}
+
+	private void paintSelection(final Graphics graphics) {
+		if (!hasSelection()) {
+			return;
+		}
+		final ContentRange selectedRange = selector.getRange();
+		final IBox selectionRootBox = contentTopology.findBoxForRange(selectedRange);
+		if (selectionRootBox == null) {
+			return; // The selection is invalid/unbalanced
+		}
+		selectionRootBox.accept(new DepthFirstBoxTraversal<Object>() {
+			@Override
+			public Object visit(final StructuralNodeReference box) {
+				if (selectedRange.contains(box.getRange())) {
+					box.highlight(graphics, SELECTION_FOREGROUND_COLOR, SELECTION_BACKGROUND_COLOR);
+					return null;
+				}
+				return super.visit(box);
+			}
+
+			@Override
+			public Object visit(final InlineNodeReference box) {
+				if (selectedRange.contains(box.getNode().getRange())) {
+					box.highlight(graphics, SELECTION_FOREGROUND_COLOR, SELECTION_BACKGROUND_COLOR);
+					return null;
+				}
+				return super.visit(box);
+			}
+
+			@Override
+			public Object visit(final TextContent box) {
+				if (selectedRange.intersects(box.getRange())) {
+					box.highlight(graphics, selectedRange.getStartOffset(), selectedRange.getEndOffset(), SELECTION_FOREGROUND_COLOR, SELECTION_BACKGROUND_COLOR);
+				}
+				return null;
+			}
+
+			@Override
+			public Object visit(final NodeEndOffsetPlaceholder box) {
+				if (selectedRange.contains(box.getRange())) {
+					box.highlight(graphics, SELECTION_FOREGROUND_COLOR, SELECTION_BACKGROUND_COLOR);
+					return null;
+				}
+				return null;
+			}
+		});
+	}
+
+	private Caret getCaretForBox(final Graphics graphics, final IContentBox box, final int offset) {
+		return box.accept(new BaseBoxVisitorWithResult<Caret>() {
+			@Override
+			public Caret visit(final StructuralNodeReference box) {
+				return getCaretForStructuralNode(graphics, box, offset);
+			}
+
+			@Override
+			public Caret visit(final InlineNodeReference box) {
+				return getCaretForInlineNode(graphics, box, offset);
+			}
+
+			@Override
+			public Caret visit(final TextContent box) {
+				return getCaretForText(graphics, box, offset);
+			}
+
+			@Override
+			public Caret visit(final NodeEndOffsetPlaceholder box) {
+				return getCaretForEndOffsetPlaceholder(graphics, box, offset);
+			}
+		});
+	}
+
+	private Caret getCaretForStructuralNode(final Graphics graphics, final StructuralNodeReference box, final int offset) {
+		final Rectangle area = getAbsolutePositionArea(graphics, box, offset);
+		if (box.isAtStart(offset)) {
+			return new InsertBeforeStructuralNodeCaret(area, box.getNode());
+		} else if (box.isAtEnd(offset) && box.canContainText()) {
+			return new AppendNodeWithTextCaret(area, box.getNode(), box.isEmpty());
+		} else if (box.isAtEnd(offset) && !box.canContainText()) {
+			return new AppendStructuralNodeCaret(area, box.getNode());
+		} else {
+			return null;
+		}
+	}
+
+	private Rectangle getAbsolutePositionArea(final Graphics graphics, final IContentBox box, final int offset) {
+		if (box == null) {
+			return Rectangle.NULL;
+		}
+		return box.accept(new BaseBoxVisitorWithResult<Rectangle>() {
+			@Override
+			public Rectangle visit(final StructuralNodeReference box) {
+				if (box.isAtStart(offset)) {
+					return makeAbsolute(box.getPositionArea(graphics, offset), box);
+				} else if (box.isAtEnd(offset) && box.canContainText() && !box.isEmpty()) {
+					final int lastOffset = offset - 1;
+					final IContentBox lastBox = contentTopology.findBoxForPosition(lastOffset, box);
+					return makeAbsolute(lastBox.getPositionArea(graphics, lastOffset), lastBox);
+				} else if (box.isAtEnd(offset) && box.canContainText() && box.isEmpty()) {
+					final IBox lastLowestChild = findDeepestLastInlineChildBox(box);
+					if (lastLowestChild != null) {
+						return new Rectangle(lastLowestChild.getAbsoluteLeft(), lastLowestChild.getAbsoluteTop(), lastLowestChild.getWidth(), lastLowestChild.getHeight());
+					} else {
+						return makeAbsolute(box.getPositionArea(graphics, offset), box);
+					}
+				} else if (box.isAtEnd(offset)) {
+					return makeAbsolute(box.getPositionArea(graphics, offset), box);
+				} else {
+					return Rectangle.NULL;
+				}
+			}
+
+			@Override
+			public Rectangle visit(final InlineNodeReference box) {
+				if (box.isAtStart(offset)) {
+					return makeAbsolute(box.getPositionArea(graphics, offset), box);
+				} else if (box.isAtEnd(offset) && box.canContainText() && !box.isEmpty()) {
+					final int lastOffset = offset - 1;
+					final IContentBox lastBox = contentTopology.findBoxForPosition(lastOffset, box);
+					return getAbsolutePositionArea(graphics, lastBox, lastOffset);
+				} else if (box.isAtEnd(offset) && box.canContainText() && box.isEmpty()) {
+					final IBox lastLowestChild = findDeepestLastInlineChildBox(box);
+					if (lastLowestChild != null) {
+						return makeAbsolute(lastLowestChild.getBounds(), lastLowestChild);
+					} else {
+						return makeAbsolute(box.getPositionArea(graphics, offset), box);
+					}
+				} else if (box.isAtEnd(offset)) {
+					return makeAbsolute(box.getPositionArea(graphics, offset), box);
+				} else {
+					return Rectangle.NULL;
+				}
+			}
+
+			@Override
+			public Rectangle visit(final TextContent box) {
+				return makeAbsolute(box.getPositionArea(graphics, offset), box);
+			}
+
+			@Override
+			public Rectangle visit(final NodeEndOffsetPlaceholder box) {
+				return makeAbsolute(box.getPositionArea(graphics, offset), box);
+			}
+		});
+	}
+
+	private static Rectangle makeAbsolute(final Rectangle rectangle, final IBox box) {
+		return new Rectangle(rectangle.getX() + box.getAbsoluteLeft(), rectangle.getY() + box.getAbsoluteTop(), rectangle.getWidth(), rectangle.getHeight());
+	}
+
+	private static IInlineBox findDeepestLastInlineChildBox(final IBox startBox) {
+		final IInlineBox[] deepestLastChildBox = new IInlineBox[1];
+		startBox.accept(new DepthFirstBoxTraversal<IBox>() {
+			@Override
+			public IBox visit(final Square box) {
+				deepestLastChildBox[0] = box;
+				return null;
+			}
+
+			@Override
+			public IBox visit(final StaticText box) {
+				deepestLastChildBox[0] = box;
+				return null;
+			}
+
+			@Override
+			public IBox visit(final Image box) {
+				deepestLastChildBox[0] = box;
+				return null;
+			}
+
+			@Override
+			public IBox visit(final TextContent box) {
+				deepestLastChildBox[0] = box;
+				return null;
+			}
+
+			@Override
+			public IBox visit(final NodeEndOffsetPlaceholder box) {
+				deepestLastChildBox[0] = box;
+				return null;
+			}
+
+			@Override
+			public IBox visit(final GraphicalBullet box) {
+				deepestLastChildBox[0] = box;
+				return null;
+			}
+
+			@Override
+			public IBox visit(final NodeTag box) {
+				deepestLastChildBox[0] = box;
+				return null;
+			}
+		});
+		return deepestLastChildBox[0];
+	}
+
+	private Caret getCaretForInlineNode(final Graphics graphics, final InlineNodeReference box, final int offset) {
+		final Rectangle area = getAbsolutePositionArea(graphics, box, offset);
+		if (box.getNode().getStartOffset() == offset) {
+			return new InsertBeforeInlineNodeCaret(area, box.getNode());
+		} else if (box.getNode().getEndOffset() == offset) {
+			return new AppendNodeWithTextCaret(area, box.getNode(), box.isEmpty() && box.canContainText());
+		} else {
+			return new IntermediateInlineCaret(area, box.isAtStart(offset));
+		}
+	}
+
+	private Caret getCaretForText(final Graphics graphics, final TextContent box, final int offset) {
+		if (box.getStartOffset() > offset || box.getEndOffset() < offset) {
+			return null;
+		}
+		final Rectangle relativeArea = box.getPositionArea(graphics, offset);
+		final Rectangle area = new Rectangle(relativeArea.getX() + box.getAbsoluteLeft(), relativeArea.getY() + box.getAbsoluteTop(), relativeArea.getWidth(), relativeArea.getHeight());
+		final FontSpec font = box.getFont();
+		final String character = box.getText().substring(offset - box.getStartOffset(), offset - box.getStartOffset() + 1);
+		return new TextCaret(area, font, character, false);
+	}
+
+	private Caret getCaretForEndOffsetPlaceholder(final Graphics graphics, final NodeEndOffsetPlaceholder box, final int offset) {
+		final Rectangle area = getAbsolutePositionArea(graphics, box, offset);
+		return new AppendNodeWithTextCaret(area, box.getNode(), box.isEmpty());
+	}
+
+	private static interface Caret {
+		Rectangle getHotArea();
+
+		Rectangle getVisibleArea();
+
+		void paint(Graphics graphics);
+	}
+
+	private static class InsertBeforeStructuralNodeCaret implements Caret {
+		private final Rectangle area;
+		private final INode node;
+
+		public InsertBeforeStructuralNodeCaret(final Rectangle area, final INode node) {
+			this.area = area;
+			this.node = node;
+		}
+
+		@Override
+		public Rectangle getHotArea() {
+			return new Rectangle(area.getX(), area.getY(), 1, 1);
+		}
+
+		@Override
+		public Rectangle getVisibleArea() {
+			return new Rectangle(area.getX(), area.getY(), area.getWidth(), CARET_BUFFER);
+		}
+
+		@Override
+		public void paint(final Graphics graphics) {
+			if (area == Rectangle.NULL) {
+				return;
+			}
+
+			graphics.setForeground(graphics.getColor(CARET_FOREGROUND_COLOR));
+			graphics.setBackground(graphics.getColor(CARET_BACKGROUND_COLOR));
+
+			final int x = area.getX();
+			final int y = area.getY();
+
+			graphics.swapColors();
+			graphics.fillRect(x - 2, y - 2, area.getWidth(), 6);
+			graphics.fillRect(x - 2, y - 2, 6, area.getHeight());
+			graphics.swapColors();
+
+			graphics.fillRect(x, y, area.getWidth(), 2);
+			graphics.fillRect(x, y, 2, area.getHeight());
+
+			NodeGraphics.drawStartTag(graphics, node, x + 5, y + 5, false, false);
+		}
+
+	}
+
+	private static class InsertBeforeInlineNodeCaret implements Caret {
+		private final Rectangle area;
+		private final INode node;
+
+		public InsertBeforeInlineNodeCaret(final Rectangle area, final INode node) {
+			this.area = area;
+			this.node = node;
+		}
+
+		@Override
+		public Rectangle getHotArea() {
+			return new Rectangle(area.getX(), area.getY(), 1, 1);
+		}
+
+		@Override
+		public Rectangle getVisibleArea() {
+			return new Rectangle(area.getX(), area.getY(), area.getWidth(), CARET_BUFFER);
+		}
+
+		@Override
+		public void paint(final Graphics graphics) {
+			if (area == Rectangle.NULL) {
+				return;
+			}
+
+			graphics.setForeground(graphics.getColor(CARET_FOREGROUND_COLOR));
+			graphics.setBackground(graphics.getColor(CARET_BACKGROUND_COLOR));
+
+			final int x = area.getX();
+			final int y = area.getY();
+
+			graphics.fillRect(x, y, 2, area.getHeight());
+
+			NodeGraphics.drawStartTag(graphics, node, x + 5, y + area.getHeight() / 2, true, false);
+		}
+	}
+
+	private static class AppendNodeWithTextCaret implements Caret {
+		private final Rectangle area;
+		private final INode node;
+		private final boolean nodeIsEmpty;
+
+		public AppendNodeWithTextCaret(final Rectangle area, final INode node, final boolean nodeIsEmpty) {
+			this.area = area;
+			this.node = node;
+			this.nodeIsEmpty = nodeIsEmpty;
+		}
+
+		@Override
+		public Rectangle getHotArea() {
+			return new Rectangle(getX(), area.getY(), 1, area.getHeight());
+		}
+
+		@Override
+		public Rectangle getVisibleArea() {
+			return area;
+		}
+
+		@Override
+		public void paint(final Graphics graphics) {
+			if (area == Rectangle.NULL) {
+				return;
+			}
+
+			graphics.setForeground(graphics.getColor(CARET_FOREGROUND_COLOR));
+			graphics.setBackground(graphics.getColor(CARET_BACKGROUND_COLOR));
+
+			final int x = getX();
+			final int y = area.getY();
+
+			graphics.fillRect(x, y, 2, area.getHeight());
+
+			NodeGraphics.drawEndTag(graphics, node, x + 5, y + area.getHeight() / 2, true, false);
+		}
+
+		private int getX() {
+			return nodeIsEmpty ? area.getX() : area.getX() + area.getWidth();
+		}
+	}
+
+	private static class IntermediateInlineCaret implements Caret {
+		private final Rectangle area;
+		private final boolean isAtBoxStart;
+
+		public IntermediateInlineCaret(final Rectangle area, final boolean isAtBoxStart) {
+			this.area = area;
+			this.isAtBoxStart = isAtBoxStart;
+		}
+
+		@Override
+		public Rectangle getHotArea() {
+			return new Rectangle(getX(), area.getY(), 1, area.getHeight());
+		}
+
+		@Override
+		public Rectangle getVisibleArea() {
+			return area;
+		}
+
+		@Override
+		public void paint(final Graphics graphics) {
+			if (area == Rectangle.NULL) {
+				return;
+			}
+
+			graphics.setForeground(graphics.getColor(CARET_FOREGROUND_COLOR));
+			graphics.setBackground(graphics.getColor(CARET_BACKGROUND_COLOR));
+
+			final int x = getX();
+			final int y = area.getY();
+
+			graphics.fillRect(x, y, 2, area.getHeight());
+		}
+
+		private int getX() {
+			return isAtBoxStart ? area.getX() : area.getX() + area.getWidth();
+		}
+	}
+
+	private static class AppendStructuralNodeCaret implements Caret {
+		private final Rectangle area;
+		private final INode node;
+
+		public AppendStructuralNodeCaret(final Rectangle area, final INode node) {
+			this.area = area;
+			this.node = node;
+		}
+
+		@Override
+		public Rectangle getHotArea() {
+			return new Rectangle(area.getX() + area.getWidth() - 1, area.getY() + area.getHeight() - 1, 1, 1);
+		}
+
+		@Override
+		public Rectangle getVisibleArea() {
+			return new Rectangle(area.getX(), area.getY() + area.getHeight(), area.getWidth(), CARET_BUFFER);
+		}
+
+		@Override
+		public void paint(final Graphics graphics) {
+			if (area == Rectangle.NULL) {
+				return;
+			}
+
+			graphics.setForeground(graphics.getColor(CARET_FOREGROUND_COLOR));
+			graphics.setBackground(graphics.getColor(CARET_BACKGROUND_COLOR));
+
+			final int x = area.getX();
+			final int y = area.getY() + area.getHeight();
+
+			graphics.swapColors();
+			graphics.fillRect(x - 2, y - 2, area.getWidth(), 6);
+			graphics.fillRect(x - 2, y - 2, 6, area.getHeight());
+			graphics.swapColors();
+
+			graphics.fillRect(x, y, area.getWidth(), 2);
+			graphics.fillRect(x + area.getWidth() - 2, y, 2, -area.getHeight());
+
+			NodeGraphics.drawEndTag(graphics, node, x + 5, y + 5, false, false);
+		}
+	}
+
+	private static class TextCaret implements Caret {
+		private final Rectangle area;
+		private final FontSpec font;
+		private final String character;
+		private final boolean overwrite;
+
+		public TextCaret(final Rectangle area, final FontSpec font, final String character, final boolean overwrite) {
+			this.area = area;
+			this.font = font;
+			this.character = character;
+			this.overwrite = overwrite;
+		}
+
+		@Override
+		public Rectangle getHotArea() {
+			return area;
+		}
+
+		@Override
+		public Rectangle getVisibleArea() {
+			return new Rectangle(area.getX(), area.getY(), area.getWidth(), area.getHeight() + 2);
+		}
+
+		@Override
+		public void paint(final Graphics graphics) {
+			if (area == Rectangle.NULL) {
+				return;
+			}
+
+			graphics.setForeground(graphics.getColor(CARET_FOREGROUND_COLOR));
+			graphics.setBackground(graphics.getColor(CARET_BACKGROUND_COLOR));
+
+			if (overwrite) {
+				graphics.fillRect(area.getX(), area.getY(), area.getWidth(), area.getHeight());
+				graphics.setCurrentFont(graphics.getFont(font));
+				graphics.drawString(character, area.getX(), area.getY());
+			} else {
+				graphics.fillRect(area.getX() - 1, area.getY(), 2, area.getHeight());
+			}
+		}
+	}
+
+	private static class MoveWithSelection {
+		public final ICursorMove move;
+		public final boolean select;
+
+		public MoveWithSelection(final ICursorMove move, final boolean select) {
+			this.move = move;
+			this.select = select;
+		}
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/CursorMoves.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/CursorMoves.java
new file mode 100644
index 0000000..3ee7377
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/CursorMoves.java
@@ -0,0 +1,90 @@
+/*******************************************************************************
+ * 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;
+
+/**
+ * @author Florian Thienel
+ */
+public class CursorMoves {
+
+	private static final ICursorMove LEFT = new MoveLeft();
+	private static final ICursorMove RIGHT = new MoveRight();
+	private static final ICursorMove UP = new MoveUp();
+	private static final ICursorMove DOWN = new MoveDown();
+	private static final ICursorMove TO_PREVIOUS_PAGE = new MoveToPreviousPage();
+	private static final ICursorMove TO_NEXT_PAGE = new MoveToNextPage();
+	private static final ICursorMove TO_WORD_START = new MoveToWordStart();
+	private static final ICursorMove TO_WORD_END = new MoveToWordEnd();
+	private static final ICursorMove TO_NEXT_WORD = new MoveToNextWord();
+	private static final ICursorMove TO_PREVIOUS_WORD = new MoveToPreviousWord();
+	private static final ICursorMove TO_LINE_START = new MoveToLineStart();
+	private static final ICursorMove TO_LINE_END = new MoveToLineEnd();
+
+	public static ICursorMove toOffset(final int offset) {
+		return new MoveToOffset(offset);
+	}
+
+	public static ICursorMove toAbsoluteCoordinates(final int x, final int y) {
+		return new MoveToAbsoluteCoordinates(x, y);
+	}
+
+	public static ICursorMove by(final int distance) {
+		return new MoveBy(distance);
+	}
+
+	public static ICursorMove left() {
+		return LEFT;
+	}
+
+	public static ICursorMove right() {
+		return RIGHT;
+	}
+
+	public static ICursorMove up() {
+		return UP;
+	}
+
+	public static ICursorMove down() {
+		return DOWN;
+	}
+
+	public static ICursorMove toPreviousPage() {
+		return TO_PREVIOUS_PAGE;
+	}
+
+	public static ICursorMove toNextPage() {
+		return TO_NEXT_PAGE;
+	}
+
+	public static ICursorMove toWordStart() {
+		return TO_WORD_START;
+	}
+
+	public static ICursorMove toWordEnd() {
+		return TO_WORD_END;
+	}
+
+	public static ICursorMove toNextWord() {
+		return TO_NEXT_WORD;
+	}
+
+	public static ICursorMove toPreviousWord() {
+		return TO_PREVIOUS_WORD;
+	}
+
+	public static ICursorMove toLineStart() {
+		return TO_LINE_START;
+	}
+
+	public static ICursorMove toLineEnd() {
+		return TO_LINE_END;
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/IContentSelector.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/IContentSelector.java
new file mode 100644
index 0000000..6065a72
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/IContentSelector.java
@@ -0,0 +1,35 @@
+/*******************************************************************************
+ * 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 interface IContentSelector {
+
+	void setMark(int offset);
+
+	void moveEndTo(int offset);
+
+	void setEndAbsoluteTo(int offset);
+
+	boolean isActive();
+
+	int getStartOffset();
+
+	int getEndOffset();
+
+	ContentRange getRange();
+
+	int getCaretOffset();
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/ICursor.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/ICursor.java
new file mode 100644
index 0000000..2228287
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/ICursor.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.cursor;
+
+import org.eclipse.vex.core.provisional.dom.ContentRange;
+
+public interface ICursor {
+
+	int getOffset();
+
+	boolean hasSelection();
+
+	ContentRange getSelectedRange();
+
+	void addPositionListener(ICursorPositionListener listener);
+
+	void removePositionListener(ICursorPositionListener listener);
+
+	void move(ICursorMove move);
+
+	void select(ICursorMove move);
+
+}
\ No newline at end of file
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/ICursorMove.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/ICursorMove.java
new file mode 100644
index 0000000..c9cdcef
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/ICursorMove.java
@@ -0,0 +1,28 @@
+/*******************************************************************************
+ * 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.internal.boxes.IContentBox;
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+import org.eclipse.vex.core.internal.widget.IViewPort;
+
+/**
+ * @author Florian Thienel
+ */
+public interface ICursorMove {
+
+	int calculateNewOffset(final Graphics graphics, IViewPort viewPort, final ContentTopology contentTopology, final int currentOffset, final IContentBox currentBox, final Rectangle hotArea, int preferredX);
+
+	boolean preferX();
+
+	boolean isAbsolute();
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/ICursorPositionListener.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/ICursorPositionListener.java
new file mode 100644
index 0000000..9f33784
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/ICursorPositionListener.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.cursor;
+
+/**
+ * @author Florian Thienel
+ */
+public interface ICursorPositionListener {
+
+	void positionAboutToChange();
+
+	void positionChanged(int offset);
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveBy.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveBy.java
new file mode 100644
index 0000000..bbc4909
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveBy.java
@@ -0,0 +1,40 @@
+/*******************************************************************************
+ * 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.cursor;
+
+import org.eclipse.vex.core.internal.boxes.IContentBox;
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+import org.eclipse.vex.core.internal.widget.IViewPort;
+
+public class MoveBy implements ICursorMove {
+
+	private final int distance;
+
+	public MoveBy(final int distance) {
+		this.distance = distance;
+	}
+
+	@Override
+	public int calculateNewOffset(final Graphics graphics, final IViewPort viewPort, final ContentTopology contentTopology, final int currentOffset, final IContentBox currentBox, final Rectangle hotArea, final int preferredX) {
+		return Math.max(0, Math.min(currentOffset + distance, contentTopology.getLastOffset()));
+	}
+
+	@Override
+	public boolean preferX() {
+		return true;
+	}
+
+	@Override
+	public boolean isAbsolute() {
+		return false;
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveDown.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveDown.java
new file mode 100644
index 0000000..b134f0f
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveDown.java
@@ -0,0 +1,265 @@
+/*******************************************************************************
+ * 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 static org.eclipse.vex.core.internal.cursor.ContentTopology.findClosestContentBoxChildBelow;
+import static org.eclipse.vex.core.internal.cursor.ContentTopology.getParentContentBox;
+
+import org.eclipse.vex.core.internal.boxes.BaseBoxVisitorWithResult;
+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.StructuralNodeReference;
+import org.eclipse.vex.core.internal.boxes.TextContent;
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+import org.eclipse.vex.core.internal.widget.IViewPort;
+
+/**
+ * @author Florian Thienel
+ */
+public class MoveDown implements ICursorMove {
+
+	@Override
+	public boolean preferX() {
+		return false;
+	}
+
+	@Override
+	public boolean isAbsolute() {
+		return false;
+	}
+
+	@Override
+	public int calculateNewOffset(final Graphics graphics, final IViewPort viewPort, final ContentTopology contentTopology, final int currentOffset, final IContentBox currentBox, final Rectangle hotArea, final int preferredX) {
+		if (isAtStartOfEmptyBox(currentOffset, currentBox)) {
+			return currentBox.getEndOffset();
+		}
+		if (isAtStartOfBoxWithChildren(currentOffset, currentBox)) {
+			final IContentBox firstChild = getFirstContentBoxChild(currentBox);
+			if (firstChild != null) {
+				if (containsInlineContent(currentBox)) {
+					return findOffsetInNextBoxBelow(graphics, currentOffset, firstChild, preferredX, hotArea.getY() - 1);
+				} else if (containsInlineContent(firstChild)) {
+					return findOffsetInNextBoxBelow(graphics, currentOffset, firstChild, preferredX, currentBox.getAbsoluteTop() - 1);
+				} else {
+					return firstChild.getStartOffset();
+				}
+			}
+		}
+
+		return findOffsetInNextBoxBelow(graphics, currentOffset, currentBox, preferredX, hotArea.getY() + hotArea.getHeight() - 1);
+	}
+
+	private static boolean isAtStartOfEmptyBox(final int offset, final IContentBox box) {
+		return box.isAtStart(offset) && box.isEmpty() && box.getEndOffset() > box.getStartOffset();
+	}
+
+	private static boolean isAtStartOfBoxWithChildren(final int offset, final IContentBox box) {
+		return box.isAtStart(offset) && canHaveChildren(box);
+	}
+
+	private static boolean canHaveChildren(final IContentBox box) {
+		return box.accept(new BaseBoxVisitorWithResult<Boolean>(false) {
+			@Override
+			public Boolean visit(final StructuralNodeReference box) {
+				return true;
+			}
+
+			@Override
+			public Boolean visit(final InlineNodeReference box) {
+				return true;
+			}
+		});
+	}
+
+	private static boolean containsInlineContent(final IContentBox box) {
+		return box.accept(new BaseBoxVisitorWithResult<Boolean>(false) {
+			@Override
+			public Boolean visit(final StructuralNodeReference box) {
+				return box.containsInlineContent();
+			}
+
+			@Override
+			public Boolean visit(final InlineNodeReference box) {
+				return true;
+			}
+
+			@Override
+			public Boolean visit(final TextContent box) {
+				return true;
+			}
+
+			@Override
+			public Boolean visit(final NodeEndOffsetPlaceholder box) {
+				return true;
+			}
+		});
+	}
+
+	private static IContentBox getFirstContentBoxChild(final IContentBox parent) {
+		return parent.accept(new DepthFirstBoxTraversal<IContentBox>() {
+			private IContentBox firstChild;
+
+			@Override
+			public IContentBox visit(final StructuralNodeReference box) {
+				if (box == parent) {
+					super.visit(box);
+					return firstChild;
+				}
+
+				if (firstChild == null) {
+					firstChild = box;
+				}
+				return box;
+			}
+
+			@Override
+			public IContentBox visit(final InlineNodeReference box) {
+				if (box == parent) {
+					super.visit(box);
+					return firstChild;
+				}
+
+				if (firstChild == null) {
+					firstChild = box;
+				}
+				return box;
+			}
+
+			@Override
+			public IContentBox visit(final TextContent box) {
+				if (box == parent) {
+					return null;
+				}
+
+				if (firstChild == null) {
+					firstChild = box;
+				}
+				return box;
+			}
+
+			@Override
+			public IContentBox visit(final NodeEndOffsetPlaceholder box) {
+				if (box == parent) {
+					return null;
+				}
+
+				if (firstChild == null) {
+					firstChild = box;
+				}
+				return box;
+			}
+		});
+	}
+
+	private int findOffsetInNextBoxBelow(final Graphics graphics, final int currentOffset, final IContentBox currentBox, final int x, final int y) {
+		final IContentBox nextBoxBelow = findNextContentBoxBelow(currentBox, x, y);
+		return findOffsetInBox(graphics, currentOffset, x, y, nextBoxBelow);
+	}
+
+	private static int findOffsetInBox(final Graphics graphics, final int currentOffset, final int hotX, final int hotY, final IContentBox box) {
+		if (box.isEmpty()) {
+			return box.getStartOffset();
+		}
+		return box.accept(new BaseBoxVisitorWithResult<Integer>() {
+			@Override
+			public Integer visit(final StructuralNodeReference box) {
+				if (currentOffset >= box.getStartOffset()) {
+					return box.getEndOffset();
+				}
+				return box.getStartOffset();
+			}
+
+			@Override
+			public Integer visit(final InlineNodeReference box) {
+				if (currentOffset >= box.getStartOffset()) {
+					return box.getEndOffset();
+				}
+				return box.getStartOffset();
+			}
+
+			@Override
+			public Integer visit(final TextContent box) {
+				return box.getOffsetForCoordinates(graphics, hotX - box.getAbsoluteLeft(), hotY - box.getAbsoluteTop());
+			}
+
+			@Override
+			public Integer visit(final NodeEndOffsetPlaceholder box) {
+				return box.getOffsetForCoordinates(graphics, hotX - box.getAbsoluteLeft(), hotY - box.getAbsoluteTop());
+			}
+		});
+	}
+
+	private static IContentBox findNextContentBoxBelow(final IContentBox currentBox, final int x, final int y) {
+		final IContentBox parent = getParentContentBox(currentBox);
+		if (parent == null) {
+			return currentBox;
+		}
+
+		final IContentBox childBelow = handleSpecialCaseMovingIntoLastLineOfParagraph(findClosestContentBoxChildBelow(parent, x, y), x, y);
+		if (childBelow == null) {
+			if (containsInlineContent(parent) || parent.isEmpty()) {
+				return findNextContentBoxBelow(parent, x, y);
+			}
+			return parent;
+		}
+
+		return childBelow;
+	}
+
+	private static IContentBox handleSpecialCaseMovingIntoLastLineOfParagraph(final IContentBox candidate, final int x, final int y) {
+		if (candidate == null) {
+			return null;
+		}
+
+		if (!candidate.isLeftOf(x)) {
+			return candidate;
+		}
+
+		final IContentBox parent = getParentContentBox(candidate);
+		if (parent == null) {
+			return candidate;
+		}
+
+		final IContentBox lastTextContentBox = parent.accept(new DepthFirstBoxTraversal<IContentBox>() {
+			private IContentBox lastTextContentBox;
+
+			@Override
+			public IContentBox visit(final StructuralNodeReference box) {
+				if (box != parent) {
+					return null;
+				}
+				super.visit(box);
+				return lastTextContentBox;
+			}
+
+			@Override
+			public IContentBox visit(final TextContent box) {
+				lastTextContentBox = box;
+				return super.visit(box);
+			}
+
+			@Override
+			public IContentBox visit(final NodeEndOffsetPlaceholder box) {
+				lastTextContentBox = box;
+				return super.visit(box);
+			}
+		});
+
+		if (candidate == lastTextContentBox) {
+			return parent;
+		}
+
+		return candidate;
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveLeft.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveLeft.java
new file mode 100644
index 0000000..9af1a28
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveLeft.java
@@ -0,0 +1,56 @@
+/*******************************************************************************
+ * 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.internal.boxes.IContentBox;
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+import org.eclipse.vex.core.internal.widget.IViewPort;
+
+/**
+ * @author Florian Thienel
+ */
+public class MoveLeft implements ICursorMove {
+
+	@Override
+	public int calculateNewOffset(final Graphics graphics, IViewPort viewPort, final ContentTopology contentTopology, final int currentOffset, final IContentBox currentBox, final Rectangle hotArea, final int preferredX) {
+		int nextOffset = Math.max(0, currentOffset - 1);
+
+		final IContentBox searchStartBox = getSearchStartBox(currentBox, nextOffset);
+		while (contentTopology.findBoxForPosition(nextOffset, searchStartBox) == null && nextOffset > 0) {
+			nextOffset = Math.max(0, nextOffset - 1);
+		}
+
+		return nextOffset;
+	}
+
+	private static IContentBox getSearchStartBox(final IContentBox currentBox, final int nextOffset) {
+		final IContentBox parentBox = ContentTopology.getParentContentBox(currentBox);
+		if (parentBox == null) {
+			return currentBox;
+		}
+		if (parentBox.getRange().contains(nextOffset)) {
+			return parentBox;
+		}
+		return getSearchStartBox(parentBox, nextOffset);
+	}
+
+	@Override
+	public boolean preferX() {
+		return true;
+	}
+
+	@Override
+	public boolean isAbsolute() {
+		return false;
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveRight.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveRight.java
new file mode 100644
index 0000000..abc9b98
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveRight.java
@@ -0,0 +1,55 @@
+/*******************************************************************************
+ * 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.internal.boxes.IContentBox;
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+import org.eclipse.vex.core.internal.widget.IViewPort;
+
+/**
+ * @author Florian Thienel
+ */
+public class MoveRight implements ICursorMove {
+
+	@Override
+	public boolean preferX() {
+		return true;
+	}
+
+	@Override
+	public boolean isAbsolute() {
+		return false;
+	}
+
+	@Override
+	public int calculateNewOffset(final Graphics graphics, IViewPort viewPort, final ContentTopology contentTopology, final int currentOffset, final IContentBox currentBox, final Rectangle hotArea, final int preferredX) {
+		int nextOffset = Math.min(currentOffset + 1, contentTopology.getLastOffset());
+
+		final IContentBox searchStartBox = getSearchStartBox(currentBox, nextOffset);
+		while (contentTopology.findBoxForPosition(nextOffset, searchStartBox) == null && nextOffset < contentTopology.getLastOffset()) {
+			nextOffset = Math.min(nextOffset + 1, contentTopology.getLastOffset());
+		}
+
+		return nextOffset;
+	}
+
+	private static IContentBox getSearchStartBox(final IContentBox currentBox, final int nextOffset) {
+		final IContentBox parentBox = ContentTopology.getParentContentBox(currentBox);
+		if (parentBox == null) {
+			return currentBox;
+		}
+		if (parentBox.getRange().contains(nextOffset)) {
+			return parentBox;
+		}
+		return getSearchStartBox(parentBox, nextOffset);
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveToAbsoluteCoordinates.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveToAbsoluteCoordinates.java
new file mode 100644
index 0000000..92968e2
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveToAbsoluteCoordinates.java
@@ -0,0 +1,72 @@
+/*******************************************************************************
+ * 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 static org.eclipse.vex.core.internal.cursor.ContentTopology.getParentContentBox;
+
+import org.eclipse.vex.core.internal.boxes.IContentBox;
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+import org.eclipse.vex.core.internal.widget.IViewPort;
+
+/**
+ * @author Florian Thienel
+ */
+public class MoveToAbsoluteCoordinates implements ICursorMove {
+
+	private final int x;
+	private final int y;
+
+	public MoveToAbsoluteCoordinates(final int x, final int y) {
+		this.x = x;
+		this.y = y;
+	}
+
+	@Override
+	public boolean preferX() {
+		return true;
+	}
+
+	@Override
+	public boolean isAbsolute() {
+		return true;
+	}
+
+	@Override
+	public int calculateNewOffset(final Graphics graphics, final IViewPort viewPort, final ContentTopology contentTopology, final int currentOffset, final IContentBox currentBox, final Rectangle hotArea, final int preferredX) {
+		final IContentBox box = contentTopology.findClosestBoxByCoordinates(x, y);
+		if (box == null) {
+			return currentOffset;
+		}
+		if (box.containsCoordinates(x, y)) {
+			return box.getOffsetForCoordinates(graphics, x - box.getAbsoluteLeft(), y - box.getAbsoluteTop());
+		} else if (box.isLeftOf(x)) {
+			if (isLastEnclosedBox(box)) {
+				return box.getEndOffset() + 1;
+			} else {
+				return box.getEndOffset();
+			}
+		} else if (box.isRightOf(x)) {
+			return box.getStartOffset();
+		} else {
+			return currentOffset;
+		}
+	}
+
+	private static boolean isLastEnclosedBox(final IContentBox enclosedBox) {
+		final IContentBox parent = getParentContentBox(enclosedBox);
+		if (parent == null) {
+			return true;
+		}
+		return enclosedBox.getEndOffset() == parent.getEndOffset() - 1;
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveToLineEnd.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveToLineEnd.java
new file mode 100644
index 0000000..2ee7cae
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveToLineEnd.java
@@ -0,0 +1,123 @@
+/*******************************************************************************
+ * 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.cursor;
+
+import org.eclipse.vex.core.internal.boxes.BaseBoxVisitorWithResult;
+import org.eclipse.vex.core.internal.boxes.DepthFirstBoxTraversal;
+import org.eclipse.vex.core.internal.boxes.IBox;
+import org.eclipse.vex.core.internal.boxes.IContentBox;
+import org.eclipse.vex.core.internal.boxes.IInlineBox;
+import org.eclipse.vex.core.internal.boxes.IParentBox;
+import org.eclipse.vex.core.internal.boxes.InlineNodeReference;
+import org.eclipse.vex.core.internal.boxes.NodeEndOffsetPlaceholder;
+import org.eclipse.vex.core.internal.boxes.Paragraph;
+import org.eclipse.vex.core.internal.boxes.ParentTraversal;
+import org.eclipse.vex.core.internal.boxes.TextContent;
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+import org.eclipse.vex.core.internal.widget.IViewPort;
+
+public class MoveToLineEnd implements ICursorMove {
+
+	@Override
+	public int calculateNewOffset(final Graphics graphics, final IViewPort viewPort, final ContentTopology contentTopology, final int currentOffset, final IContentBox currentBox, final Rectangle hotArea, final int preferredX) {
+		return currentBox.accept(new BaseBoxVisitorWithResult<Integer>(currentOffset) {
+			@Override
+			public Integer visit(final InlineNodeReference box) {
+				return getLastOffsetInLine(box, currentOffset);
+			}
+
+			@Override
+			public Integer visit(final NodeEndOffsetPlaceholder box) {
+				return getLastOffsetInLine(box, currentOffset);
+			}
+
+			@Override
+			public Integer visit(final TextContent box) {
+				return getLastOffsetInLine(box, currentOffset);
+			}
+		});
+	}
+
+	private int getLastOffsetInLine(final IInlineBox box, final int defaultValue) {
+		final int targetBaseline = getAbsoluteBaseline(box);
+		final IParentBox<IInlineBox> structuralParent = getStructuralParent(box);
+		final IContentBox lastBoxInLine = findLastContentBoxOnTargetBaseline(structuralParent, targetBaseline);
+		if (lastBoxInLine == null) {
+			return defaultValue;
+		}
+
+		return lastBoxInLine.getEndOffset();
+	}
+
+	private static IContentBox findLastContentBoxOnTargetBaseline(final IParentBox<IInlineBox> structuralParent, final int targetBaseline) {
+		IContentBox lastBox = null;
+		for (final IInlineBox child : structuralParent.getChildren()) {
+			if (targetBaseline < getAbsoluteBaseline(child)) {
+				return lastBox;
+			}
+
+			final IContentBox contentBox = findLastContentBox(child);
+			if (contentBox != null) {
+				lastBox = contentBox;
+			}
+		}
+		return ContentTopology.getParentContentBox(structuralParent);
+	}
+
+	private static IContentBox findLastContentBox(final IInlineBox box) {
+		final IContentBox[] contentBox = new IContentBox[1];
+		box.accept(new DepthFirstBoxTraversal<IContentBox>() {
+			@Override
+			public IContentBox visit(final InlineNodeReference box) {
+				contentBox[0] = box;
+				return null;
+			}
+
+			@Override
+			public IContentBox visit(final NodeEndOffsetPlaceholder box) {
+				contentBox[0] = box;
+				return null;
+			}
+
+			@Override
+			public IContentBox visit(final TextContent box) {
+				contentBox[0] = box;
+				return null;
+			}
+		});
+		return contentBox[0];
+	}
+
+	private static int getAbsoluteBaseline(final IInlineBox box) {
+		return box.getAbsoluteTop() + box.getBaseline();
+	}
+
+	private static IParentBox<IInlineBox> getStructuralParent(final IBox box) {
+		return box.accept(new ParentTraversal<IParentBox<IInlineBox>>(null) {
+			@Override
+			public IParentBox<IInlineBox> visit(final Paragraph box) {
+				return box;
+			}
+		});
+	}
+
+	@Override
+	public boolean preferX() {
+		return true;
+	}
+
+	@Override
+	public boolean isAbsolute() {
+		return true;
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveToLineStart.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveToLineStart.java
new file mode 100644
index 0000000..63287a4
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveToLineStart.java
@@ -0,0 +1,160 @@
+/*******************************************************************************
+ * 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.cursor;
+
+import org.eclipse.vex.core.internal.boxes.BaseBoxVisitorWithResult;
+import org.eclipse.vex.core.internal.boxes.DepthFirstBoxTraversal;
+import org.eclipse.vex.core.internal.boxes.IBox;
+import org.eclipse.vex.core.internal.boxes.IContentBox;
+import org.eclipse.vex.core.internal.boxes.IInlineBox;
+import org.eclipse.vex.core.internal.boxes.IParentBox;
+import org.eclipse.vex.core.internal.boxes.InlineNodeReference;
+import org.eclipse.vex.core.internal.boxes.NodeEndOffsetPlaceholder;
+import org.eclipse.vex.core.internal.boxes.Paragraph;
+import org.eclipse.vex.core.internal.boxes.ParentTraversal;
+import org.eclipse.vex.core.internal.boxes.StructuralNodeReference;
+import org.eclipse.vex.core.internal.boxes.TextContent;
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+import org.eclipse.vex.core.internal.widget.IViewPort;
+
+public class MoveToLineStart implements ICursorMove {
+
+	@Override
+	public int calculateNewOffset(final Graphics graphics, final IViewPort viewPort, final ContentTopology contentTopology, final int currentOffset, final IContentBox currentBox, final Rectangle hotArea, final int preferredX) {
+		return currentBox.accept(new BaseBoxVisitorWithResult<Integer>(currentOffset) {
+			@Override
+			public Integer visit(final StructuralNodeReference box) {
+				final Paragraph containedParagraph = getContainedParagraph(box);
+				if (containedParagraph != null) {
+					if (currentOffset == box.getEndOffset()) {
+						return getFirstOffsetInLastLine(containedParagraph, currentOffset);
+					}
+				}
+				return super.visit(box);
+			}
+
+			@Override
+			public Integer visit(final InlineNodeReference box) {
+				return getFirstOffsetInLine(box, currentOffset);
+			}
+
+			@Override
+			public Integer visit(final NodeEndOffsetPlaceholder box) {
+				return getFirstOffsetInLine(box, currentOffset);
+			}
+
+			@Override
+			public Integer visit(final TextContent box) {
+				return getFirstOffsetInLine(box, currentOffset);
+			}
+		});
+	}
+
+	private static Paragraph getContainedParagraph(final StructuralNodeReference nodeReference) {
+		return nodeReference.accept(new DepthFirstBoxTraversal<Paragraph>() {
+			@Override
+			public Paragraph visit(final StructuralNodeReference box) {
+				if (box == nodeReference) {
+					return super.visit(box);
+				}
+				return null;
+			}
+
+			@Override
+			public Paragraph visit(final Paragraph box) {
+				return box;
+			}
+		});
+	}
+
+	private static int getFirstOffsetInLastLine(final Paragraph box, final int defaultValue) {
+		int currentBaseline = 0;
+		IContentBox firstChildOnLine = null;
+		for (final IInlineBox child : box.getChildren()) {
+			final int baseline = getAbsoluteBaseline(child);
+			if (baseline > currentBaseline && child instanceof IContentBox) {
+				currentBaseline = baseline;
+				firstChildOnLine = (IContentBox) child;
+			}
+		}
+		if (firstChildOnLine == null) {
+			return defaultValue;
+		}
+		return firstChildOnLine.getStartOffset();
+	}
+
+	private static int getFirstOffsetInLine(final IInlineBox box, final int defaultValue) {
+		final int targetBaseline = getAbsoluteBaseline(box);
+		final IParentBox<IInlineBox> structuralParent = getStructuralParent(box);
+		final IContentBox firstBoxInLine = findFirstContentBoxOnTargetBaseline(structuralParent, targetBaseline);
+		if (firstBoxInLine == null) {
+			return defaultValue;
+		}
+
+		return firstBoxInLine.getStartOffset();
+	}
+
+	private static int getAbsoluteBaseline(final IInlineBox box) {
+		return box.getAbsoluteTop() + box.getBaseline();
+	}
+
+	private static IContentBox findFirstContentBoxOnTargetBaseline(final IParentBox<IInlineBox> structuralParent, final int targetBaseline) {
+		for (final IInlineBox child : structuralParent.getChildren()) {
+			if (targetBaseline == getAbsoluteBaseline(child)) {
+				final IContentBox contentBox = findFirstContentBox(child);
+				if (contentBox != null) {
+					return contentBox;
+				}
+			}
+		}
+		return null;
+	}
+
+	private static IContentBox findFirstContentBox(final IInlineBox box) {
+		return box.accept(new DepthFirstBoxTraversal<IContentBox>() {
+			@Override
+			public IContentBox visit(final InlineNodeReference box) {
+				return box;
+			}
+
+			@Override
+			public IContentBox visit(final NodeEndOffsetPlaceholder box) {
+				return box;
+			}
+
+			@Override
+			public IContentBox visit(final TextContent box) {
+				return box;
+			}
+		});
+	}
+
+	private static IParentBox<IInlineBox> getStructuralParent(final IBox box) {
+		return box.accept(new ParentTraversal<IParentBox<IInlineBox>>(null) {
+			@Override
+			public IParentBox<IInlineBox> visit(final Paragraph box) {
+				return box;
+			}
+		});
+	}
+
+	@Override
+	public boolean preferX() {
+		return true;
+	}
+
+	@Override
+	public boolean isAbsolute() {
+		return true;
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveToNextPage.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveToNextPage.java
new file mode 100644
index 0000000..f0bcddf
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveToNextPage.java
@@ -0,0 +1,102 @@
+/*******************************************************************************
+ * 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.cursor;
+
+import static org.eclipse.vex.core.internal.cursor.ContentTopology.findClosestContentBoxChildBelow;
+import static org.eclipse.vex.core.internal.cursor.ContentTopology.getParentContentBox;
+
+import org.eclipse.vex.core.internal.boxes.BaseBoxVisitorWithResult;
+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.StructuralNodeReference;
+import org.eclipse.vex.core.internal.boxes.TextContent;
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+import org.eclipse.vex.core.internal.widget.IViewPort;
+
+public class MoveToNextPage implements ICursorMove {
+
+	@Override
+	public int calculateNewOffset(final Graphics graphics, final IViewPort viewPort, final ContentTopology contentTopology, final int currentOffset, final IContentBox currentBox, final Rectangle hotArea, final int preferredX) {
+		final int x = preferredX;
+		final int y = viewPort.getVisibleArea().getY() + Math.round(viewPort.getVisibleArea().getHeight() * 1.9f);
+
+		final IContentBox outmostContentBox = contentTopology.getOutmostContentBox();
+		if (y > outmostContentBox.getAbsoluteTop() + outmostContentBox.getHeight()) {
+			return outmostContentBox.getEndOffset();
+		}
+
+		final IContentBox box = contentTopology.findClosestBoxByCoordinates(x, y);
+		if (box == null) {
+			return currentOffset;
+		} else if (box.containsCoordinates(x, y)) {
+			return findBestOffsetWithin(graphics, box, x, y);
+		} else if (box.isLeftOf(x)) {
+			if (isLastEnclosedBox(box)) {
+				return box.getEndOffset() + 1;
+			} else {
+				return box.getEndOffset();
+			}
+		} else if (box.isRightOf(x)) {
+			return box.getStartOffset();
+		} else {
+			return currentOffset;
+		}
+	}
+
+	private static int findBestOffsetWithin(final Graphics graphics, final IContentBox closestBoxByCoordinates, final int x, final int y) {
+		return closestBoxByCoordinates.accept(new BaseBoxVisitorWithResult<Integer>() {
+			@Override
+			public Integer visit(final StructuralNodeReference box) {
+				final IContentBox closestChild = findClosestContentBoxChildBelow(box, x, y);
+				if (closestChild == null) {
+					return box.getEndOffset();
+				}
+				return closestChild.accept(this);
+			}
+
+			@Override
+			public Integer visit(final InlineNodeReference box) {
+				return box.getOffsetForCoordinates(graphics, x - box.getAbsoluteLeft(), y - box.getAbsoluteTop());
+			}
+
+			@Override
+			public Integer visit(final NodeEndOffsetPlaceholder box) {
+				return box.getOffsetForCoordinates(graphics, x - box.getAbsoluteLeft(), y - box.getAbsoluteTop());
+			}
+
+			@Override
+			public Integer visit(final TextContent box) {
+				return box.getOffsetForCoordinates(graphics, x - box.getAbsoluteLeft(), y - box.getAbsoluteTop());
+			}
+		});
+	}
+
+	private static boolean isLastEnclosedBox(final IContentBox enclosedBox) {
+		final IContentBox parent = getParentContentBox(enclosedBox);
+		if (parent == null) {
+			return true;
+		}
+		return enclosedBox.getEndOffset() == parent.getEndOffset() - 1;
+	}
+
+	@Override
+	public boolean preferX() {
+		return false;
+	}
+
+	@Override
+	public boolean isAbsolute() {
+		return true;
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveToNextWord.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveToNextWord.java
new file mode 100644
index 0000000..6dbda17
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveToNextWord.java
@@ -0,0 +1,46 @@
+/*******************************************************************************
+ * 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.cursor;
+
+import org.eclipse.vex.core.internal.boxes.IContentBox;
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+import org.eclipse.vex.core.internal.widget.IViewPort;
+import org.eclipse.vex.core.provisional.dom.IContent;
+
+public class MoveToNextWord implements ICursorMove {
+
+	@Override
+	public int calculateNewOffset(final Graphics graphics, final IViewPort viewPort, final ContentTopology contentTopology, final int currentOffset, final IContentBox currentBox, final Rectangle hotArea, final int preferredX) {
+		final IContent content = currentBox.getContent();
+		final int lastOffset = contentTopology.getLastOffset();
+		int offset = currentOffset;
+		while (offset < lastOffset && Character.isLetterOrDigit(content.charAt(offset))) {
+			offset++;
+		}
+
+		while (offset < lastOffset && !Character.isLetterOrDigit(content.charAt(offset))) {
+			offset++;
+		}
+		return offset;
+	}
+
+	@Override
+	public boolean preferX() {
+		return true;
+	}
+
+	@Override
+	public boolean isAbsolute() {
+		return false;
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveToOffset.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveToOffset.java
new file mode 100644
index 0000000..d6d76c6
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveToOffset.java
@@ -0,0 +1,43 @@
+/*******************************************************************************
+ * 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.internal.boxes.IContentBox;
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+import org.eclipse.vex.core.internal.widget.IViewPort;
+
+/**
+ * @author Florian Thienel
+ */
+public class MoveToOffset implements ICursorMove {
+
+	private final int offset;
+
+	public MoveToOffset(final int offset) {
+		this.offset = offset;
+	}
+
+	@Override
+	public int calculateNewOffset(final Graphics graphics, IViewPort viewPort, final ContentTopology contentTopology, final int currentOffset, final IContentBox currentBox, final Rectangle hotArea, final int preferredX) {
+		return offset;
+	}
+
+	@Override
+	public boolean preferX() {
+		return true;
+	}
+
+	@Override
+	public boolean isAbsolute() {
+		return true;
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveToPreviousPage.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveToPreviousPage.java
new file mode 100644
index 0000000..8bb52fc
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveToPreviousPage.java
@@ -0,0 +1,102 @@
+/*******************************************************************************
+ * 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.cursor;
+
+import static org.eclipse.vex.core.internal.cursor.ContentTopology.findClosestContentBoxChildAbove;
+import static org.eclipse.vex.core.internal.cursor.ContentTopology.getParentContentBox;
+
+import org.eclipse.vex.core.internal.boxes.BaseBoxVisitorWithResult;
+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.StructuralNodeReference;
+import org.eclipse.vex.core.internal.boxes.TextContent;
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+import org.eclipse.vex.core.internal.widget.IViewPort;
+
+public class MoveToPreviousPage implements ICursorMove {
+
+	@Override
+	public int calculateNewOffset(final Graphics graphics, final IViewPort viewPort, final ContentTopology contentTopology, final int currentOffset, final IContentBox currentBox, final Rectangle hotArea, final int preferredX) {
+		final int x = preferredX;
+		final int y = viewPort.getVisibleArea().getY() - Math.round(viewPort.getVisibleArea().getHeight() * 0.9f);
+
+		final IContentBox outmostContentBox = contentTopology.getOutmostContentBox();
+		if (y < outmostContentBox.getAbsoluteTop()) {
+			return outmostContentBox.getStartOffset();
+		}
+
+		final IContentBox box = contentTopology.findClosestBoxByCoordinates(x, y);
+		if (box == null) {
+			return currentOffset;
+		} else if (box.containsCoordinates(x, y)) {
+			return findBestOffsetWithin(graphics, box, x, y);
+		} else if (box.isLeftOf(x)) {
+			if (isLastEnclosedBox(box)) {
+				return box.getEndOffset() + 1;
+			} else {
+				return box.getEndOffset();
+			}
+		} else if (box.isRightOf(x)) {
+			return box.getStartOffset();
+		} else {
+			return currentOffset;
+		}
+	}
+
+	private static int findBestOffsetWithin(final Graphics graphics, final IContentBox closestBoxByCoordinates, final int x, final int y) {
+		return closestBoxByCoordinates.accept(new BaseBoxVisitorWithResult<Integer>() {
+			@Override
+			public Integer visit(final StructuralNodeReference box) {
+				final IContentBox closestChild = findClosestContentBoxChildAbove(box, x, y);
+				if (closestChild == null) {
+					return box.getStartOffset();
+				}
+				return closestChild.accept(this);
+			}
+
+			@Override
+			public Integer visit(final InlineNodeReference box) {
+				return box.getOffsetForCoordinates(graphics, x - box.getAbsoluteLeft(), y - box.getAbsoluteTop());
+			}
+
+			@Override
+			public Integer visit(final NodeEndOffsetPlaceholder box) {
+				return box.getOffsetForCoordinates(graphics, x - box.getAbsoluteLeft(), y - box.getAbsoluteTop());
+			}
+
+			@Override
+			public Integer visit(final TextContent box) {
+				return box.getOffsetForCoordinates(graphics, x - box.getAbsoluteLeft(), y - box.getAbsoluteTop());
+			}
+		});
+	}
+
+	private static boolean isLastEnclosedBox(final IContentBox enclosedBox) {
+		final IContentBox parent = getParentContentBox(enclosedBox);
+		if (parent == null) {
+			return true;
+		}
+		return enclosedBox.getEndOffset() == parent.getEndOffset() - 1;
+	}
+
+	@Override
+	public boolean preferX() {
+		return false;
+	}
+
+	@Override
+	public boolean isAbsolute() {
+		return true;
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveToPreviousWord.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveToPreviousWord.java
new file mode 100644
index 0000000..ccfbf84
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveToPreviousWord.java
@@ -0,0 +1,35 @@
+package org.eclipse.vex.core.internal.cursor;
+
+import org.eclipse.vex.core.internal.boxes.IContentBox;
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+import org.eclipse.vex.core.internal.widget.IViewPort;
+import org.eclipse.vex.core.provisional.dom.IContent;
+
+public class MoveToPreviousWord implements ICursorMove {
+
+	@Override
+	public int calculateNewOffset(final Graphics graphics, final IViewPort viewPort, final ContentTopology contentTopology, final int currentOffset, final IContentBox currentBox, final Rectangle hotArea, final int preferredX) {
+		final IContent content = currentBox.getContent();
+		int offset = currentOffset;
+		while (offset > 1 && !Character.isLetterOrDigit(content.charAt(offset - 1))) {
+			offset--;
+		}
+
+		while (offset > 1 && Character.isLetterOrDigit(content.charAt(offset - 1))) {
+			offset--;
+		}
+		return offset;
+	}
+
+	@Override
+	public boolean preferX() {
+		return true;
+	}
+
+	@Override
+	public boolean isAbsolute() {
+		return false;
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveToWordEnd.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveToWordEnd.java
new file mode 100644
index 0000000..48523af
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveToWordEnd.java
@@ -0,0 +1,42 @@
+/*******************************************************************************
+ * 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.cursor;
+
+import org.eclipse.vex.core.internal.boxes.IContentBox;
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+import org.eclipse.vex.core.internal.widget.IViewPort;
+import org.eclipse.vex.core.provisional.dom.IContent;
+
+public class MoveToWordEnd implements ICursorMove {
+
+	@Override
+	public int calculateNewOffset(final Graphics graphics, final IViewPort viewPort, final ContentTopology contentTopology, final int currentOffset, final IContentBox currentBox, final Rectangle hotArea, final int preferredX) {
+		final IContent content = currentBox.getContent();
+		final int contentLength = content.length() - 1;
+		int offset = currentOffset;
+		while (offset < contentLength && Character.isLetterOrDigit(content.charAt(offset))) {
+			offset += 1;
+		}
+		return offset;
+	}
+
+	@Override
+	public boolean preferX() {
+		return true;
+	}
+
+	@Override
+	public boolean isAbsolute() {
+		return false;
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveToWordStart.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveToWordStart.java
new file mode 100644
index 0000000..2bd2468
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveToWordStart.java
@@ -0,0 +1,41 @@
+/*******************************************************************************
+ * 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.cursor;
+
+import org.eclipse.vex.core.internal.boxes.IContentBox;
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+import org.eclipse.vex.core.internal.widget.IViewPort;
+import org.eclipse.vex.core.provisional.dom.IContent;
+
+public class MoveToWordStart implements ICursorMove {
+
+	@Override
+	public int calculateNewOffset(final Graphics graphics, final IViewPort viewPort, final ContentTopology contentTopology, final int currentOffset, final IContentBox currentBox, final Rectangle hotArea, final int preferredX) {
+		final IContent content = currentBox.getContent();
+		int offset = currentOffset;
+		while (offset > 1 && Character.isLetterOrDigit(content.charAt(offset - 1))) {
+			offset -= 1;
+		}
+		return offset;
+	}
+
+	@Override
+	public boolean preferX() {
+		return true;
+	}
+
+	@Override
+	public boolean isAbsolute() {
+		return false;
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveUp.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveUp.java
new file mode 100644
index 0000000..fbf94b2
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveUp.java
@@ -0,0 +1,307 @@
+/*******************************************************************************
+ * 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 static org.eclipse.vex.core.internal.cursor.ContentTopology.findClosestContentBoxChildAbove;
+import static org.eclipse.vex.core.internal.cursor.ContentTopology.findHorizontallyClosestContentBox;
+import static org.eclipse.vex.core.internal.cursor.ContentTopology.getParentContentBox;
+import static org.eclipse.vex.core.internal.cursor.ContentTopology.horizontalDistance;
+import static org.eclipse.vex.core.internal.cursor.ContentTopology.removeVerticallyDistantBoxes;
+import static org.eclipse.vex.core.internal.cursor.ContentTopology.verticalDistance;
+
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.eclipse.vex.core.internal.boxes.BaseBoxVisitorWithResult;
+import org.eclipse.vex.core.internal.boxes.DepthFirstBoxTraversal;
+import org.eclipse.vex.core.internal.boxes.IBox;
+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.StructuralNodeReference;
+import org.eclipse.vex.core.internal.boxes.TextContent;
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+import org.eclipse.vex.core.internal.widget.IViewPort;
+
+/**
+ * @author Florian Thienel
+ */
+public class MoveUp implements ICursorMove {
+
+	@Override
+	public boolean preferX() {
+		return false;
+	}
+
+	@Override
+	public boolean isAbsolute() {
+		return false;
+	}
+
+	@Override
+	public int calculateNewOffset(final Graphics graphics, final IViewPort viewPort, final ContentTopology contentTopology, final int currentOffset, final IContentBox currentBox, final Rectangle hotArea, final int preferredX) {
+		if (isAtEndOfEmptyBox(currentOffset, currentBox)) {
+			return currentBox.getStartOffset();
+		}
+		if (isAtPlaceholderForEndOfEmptyBox(currentOffset, currentBox)) {
+			return getParentContentBox(currentBox).getStartOffset();
+		}
+		if (isAtEndOfBoxWithChildren(currentOffset, currentBox)) {
+			final IContentBox lastChild = getLastContentBoxChild(currentBox);
+			if (lastChild != null) {
+				if (containsInlineContent(currentBox)) {
+					return findOffsetInNextBoxAbove(graphics, currentOffset, lastChild, preferredX, hotArea.getY());
+				} else if (containsInlineContent(lastChild)) {
+					return findOffsetInNextBoxAbove(graphics, currentOffset, lastChild, preferredX, currentBox.getAbsoluteTop() + currentBox.getHeight());
+				} else {
+					return lastChild.getEndOffset();
+				}
+			}
+		}
+
+		return findOffsetInNextBoxAbove(graphics, currentOffset, currentBox, preferredX, hotArea.getY());
+	}
+
+	private static boolean isAtEndOfEmptyBox(final int offset, final IContentBox box) {
+		return box.isAtEnd(offset) && box.isEmpty() && box.getEndOffset() > box.getStartOffset();
+	}
+
+	private boolean isAtPlaceholderForEndOfEmptyBox(final int offset, final IContentBox box) {
+		final boolean isAtPlaceholder = box.accept(new BaseBoxVisitorWithResult<Boolean>(false) {
+			@Override
+			public Boolean visit(final NodeEndOffsetPlaceholder box) {
+				return true;
+			}
+		});
+		if (isAtPlaceholder) {
+			return isAtEndOfEmptyBox(offset, getParentContentBox(box));
+		}
+		return false;
+	}
+
+	private static boolean isAtEndOfBoxWithChildren(final int offset, final IContentBox box) {
+		return box.isAtEnd(offset) && canHaveChildren(box);
+	}
+
+	private static boolean canHaveChildren(final IContentBox box) {
+		return box.accept(new BaseBoxVisitorWithResult<Boolean>(false) {
+			@Override
+			public Boolean visit(final StructuralNodeReference box) {
+				return true;
+			}
+
+			@Override
+			public Boolean visit(final InlineNodeReference box) {
+				return true;
+			}
+		});
+	}
+
+	private static boolean containsInlineContent(final IContentBox box) {
+		return box.accept(new BaseBoxVisitorWithResult<Boolean>(false) {
+			@Override
+			public Boolean visit(final StructuralNodeReference box) {
+				return box.containsInlineContent();
+			}
+
+			@Override
+			public Boolean visit(final InlineNodeReference box) {
+				return true;
+			}
+
+			@Override
+			public Boolean visit(final TextContent box) {
+				return true;
+			}
+
+			@Override
+			public Boolean visit(final NodeEndOffsetPlaceholder box) {
+				return true;
+			}
+		});
+	}
+
+	private static IContentBox getLastContentBoxChild(final IContentBox parent) {
+		return parent.accept(new DepthFirstBoxTraversal<IContentBox>() {
+			private IContentBox lastChild;
+
+			@Override
+			public IContentBox visit(final StructuralNodeReference box) {
+				if (box == parent) {
+					super.visit(box);
+					return lastChild;
+				}
+				lastChild = box;
+				return null;
+			}
+
+			@Override
+			public IContentBox visit(final InlineNodeReference box) {
+				lastChild = box;
+				return super.visit(box);
+			}
+
+			@Override
+			public IContentBox visit(final TextContent box) {
+				lastChild = box;
+				return null;
+			}
+
+			@Override
+			public IContentBox visit(final NodeEndOffsetPlaceholder box) {
+				lastChild = box;
+				return super.visit(box);
+			}
+		});
+	}
+
+	private int findOffsetInNextBoxAbove(final Graphics graphics, final int currentOffset, final IContentBox currentBox, final int x, final int y) {
+		final IContentBox nextBoxAbove = findNextContentBoxAbove(currentBox, x, y);
+		return findOffsetInBox(graphics, currentOffset, x, y, nextBoxAbove);
+	}
+
+	private static int findOffsetInBox(final Graphics graphics, final int currentOffset, final int hotX, final int hotY, final IContentBox box) {
+		if (box.isEmpty() && currentOffset == box.getEndOffset()) {
+			return box.getStartOffset();
+		}
+		return box.accept(new BaseBoxVisitorWithResult<Integer>() {
+			@Override
+			public Integer visit(final StructuralNodeReference box) {
+				if (currentOffset <= box.getEndOffset()) {
+					return box.getStartOffset();
+				}
+				return box.getEndOffset();
+			}
+
+			@Override
+			public Integer visit(final InlineNodeReference box) {
+				if (currentOffset <= box.getEndOffset()) {
+					return box.getStartOffset();
+				}
+				return box.getEndOffset();
+			}
+
+			@Override
+			public Integer visit(final TextContent box) {
+				return box.getOffsetForCoordinates(graphics, hotX - box.getAbsoluteLeft(), hotY - box.getAbsoluteTop());
+			}
+
+			@Override
+			public Integer visit(final NodeEndOffsetPlaceholder box) {
+				return box.getOffsetForCoordinates(graphics, hotX - box.getAbsoluteLeft(), hotY - box.getAbsoluteTop());
+			}
+		});
+	}
+
+	private static IContentBox findNextContentBoxAbove(final IContentBox currentBox, final int x, final int y) {
+		final IContentBox parent = getParentContentBox(currentBox);
+		if (parent == null) {
+			return currentBox;
+		}
+
+		final IContentBox childAbove = handleSpecialCaseMovingIntoLastLineOfParagraph(findClosestContentBoxChildAbove(parent, x, y), x, y);
+		if (childAbove == null) {
+			return parent.accept(new BaseBoxVisitorWithResult<IContentBox>() {
+				@Override
+				public IContentBox visit(final StructuralNodeReference box) {
+					return parent;
+				}
+
+				@Override
+				public IContentBox visit(final InlineNodeReference box) {
+					return findNextContentBoxAbove(parent, x, y);
+				}
+
+				@Override
+				public IContentBox visit(final TextContent box) {
+					return findNextContentBoxAbove(parent, x, y);
+				}
+
+				@Override
+				public IContentBox visit(final NodeEndOffsetPlaceholder box) {
+					return findNextContentBoxAbove(parent, x, y);
+				}
+			});
+		}
+
+		return childAbove;
+	}
+
+	private static IContentBox handleSpecialCaseMovingIntoLastLineOfParagraph(final IContentBox candidate, final int x, final int y) {
+		if (candidate == null) {
+			return null;
+		}
+		if (!(verticalDistance(candidate, y) >= 0 && horizontalDistance(candidate, x) == 0)) {
+			return candidate;
+		}
+
+		final List<IContentBox> candidates = findVerticallyClosestTextContentChildrenAbove(candidate, y);
+		final IContentBox closestTextContentBox = findHorizontallyClosestContentBox(candidates, x);
+
+		if (closestTextContentBox != null && !closestTextContentBox.isLeftOf(x)) {
+			return closestTextContentBox;
+		}
+
+		return candidate;
+	}
+
+	private static List<IContentBox> findVerticallyClosestTextContentChildrenAbove(final IBox parent, final int y) {
+		final List<IContentBox> candidates = parent.accept(new DepthFirstBoxTraversal<List<IContentBox>>() {
+			private final LinkedList<IContentBox> candidates = new LinkedList<IContentBox>();
+			private int minVerticalDistance = Integer.MAX_VALUE;
+
+			@Override
+			public List<IContentBox> visit(final StructuralNodeReference box) {
+				if (box != parent) {
+					return Collections.emptyList();
+				}
+				super.visit(box);
+
+				removeVerticallyDistantBoxes(candidates, y, minVerticalDistance);
+
+				return candidates;
+			}
+
+			@Override
+			public List<IContentBox> visit(final TextContent box) {
+				final int distance = verticalDistance(box, y);
+				if (distance <= minVerticalDistance) {
+					minVerticalDistance = Math.min(distance, minVerticalDistance);
+					candidates.add(box);
+				}
+				if (box == parent) {
+					return candidates;
+				}
+				return null;
+			}
+
+			@Override
+			public List<IContentBox> visit(final NodeEndOffsetPlaceholder box) {
+				final int distance = verticalDistance(box, y);
+				if (distance <= minVerticalDistance) {
+					minVerticalDistance = Math.min(distance, minVerticalDistance);
+					candidates.add(box);
+				}
+				if (box == parent) {
+					return candidates;
+				}
+				return null;
+			}
+		});
+		if (candidates == null) {
+			return Collections.<IContentBox> emptyList();
+		}
+		return candidates;
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/dom/CollectingNodeTraversal.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/dom/CollectingNodeTraversal.java
new file mode 100644
index 0000000..e35d4f5
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/dom/CollectingNodeTraversal.java
@@ -0,0 +1,41 @@
+/*******************************************************************************
+ * 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.dom;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import org.eclipse.vex.core.provisional.dom.BaseNodeVisitorWithResult;
+import org.eclipse.vex.core.provisional.dom.INode;
+import org.eclipse.vex.core.provisional.dom.IParent;
+
+public class CollectingNodeTraversal<T> extends BaseNodeVisitorWithResult<T> {
+
+	public CollectingNodeTraversal() {
+		this(null);
+	}
+
+	public CollectingNodeTraversal(final T defaultValue) {
+		super(defaultValue);
+	}
+
+	protected final Collection<T> traverseChildren(final IParent parent) {
+		final ArrayList<T> result = new ArrayList<T>();
+		for (final INode child : parent.children()) {
+			final T childResult = child.accept(this);
+
+			if (childResult != null) {
+				result.add(childResult);
+			}
+		}
+		return result;
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/dom/DepthFirstNodeTraversal.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/dom/DepthFirstNodeTraversal.java
new file mode 100644
index 0000000..97ab9df
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/dom/DepthFirstNodeTraversal.java
@@ -0,0 +1,51 @@
+/*******************************************************************************
+ * 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.dom;
+
+import org.eclipse.vex.core.provisional.dom.BaseNodeVisitorWithResult;
+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;
+
+public class DepthFirstNodeTraversal<T> extends BaseNodeVisitorWithResult<T> {
+
+	public DepthFirstNodeTraversal() {
+		super(null);
+	}
+
+	@Override
+	public T visit(final IDocument document) {
+		return traverseChildren(document);
+	}
+
+	@Override
+	public T visit(final IDocumentFragment fragment) {
+		return traverseChildren(fragment);
+	}
+
+	@Override
+	public T visit(final IElement element) {
+		return traverseChildren(element);
+	}
+
+	protected final T traverseChildren(final IParent parent) {
+		for (final INode child : parent.children()) {
+			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/dom/Document.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/dom/Document.java
index de0f9bf..1affe93 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/dom/Document.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/dom/Document.java
@@ -296,9 +296,10 @@
 					throw new DocumentValidationException(MessageFormat.format("Cannot insert text ''{0}'' at offset {1}.", text, offset));
 				}
 
-				fireBeforeContentInserted(new ContentChangeEvent(Document.this, element, new ContentRange(offset, offset + adjustedText.length() - 1), false));
+				final boolean hasTextAtOffset = !(getContent().isTagMarker(offset - 1) && getContent().isTagMarker(offset));
+				fireBeforeContentInserted(new ContentChangeEvent(Document.this, element, new ContentRange(offset, offset + adjustedText.length() - 1), !hasTextAtOffset));
 				getContent().insertText(offset, adjustedText);
-				fireContentInserted(new ContentChangeEvent(Document.this, element, new ContentRange(offset, offset + adjustedText.length() - 1), false));
+				fireContentInserted(new ContentChangeEvent(Document.this, element, new ContentRange(offset, offset + adjustedText.length() - 1), !hasTextAtOffset));
 			}
 
 			@Override
@@ -310,9 +311,9 @@
 
 			@Override
 			public void visit(final IComment comment) {
-				fireBeforeContentInserted(new ContentChangeEvent(Document.this, comment.getParent(), new ContentRange(offset, offset + adjustedText.length() - 1), false));
+				fireBeforeContentInserted(new ContentChangeEvent(Document.this, comment.getParent(), new ContentRange(offset, offset + adjustedText.length() - 1), comment.isEmpty()));
 				getContent().insertText(offset, adjustedText);
-				fireContentInserted(new ContentChangeEvent(Document.this, comment.getParent(), new ContentRange(offset, offset + adjustedText.length() - 1), false));
+				fireContentInserted(new ContentChangeEvent(Document.this, comment.getParent(), new ContentRange(offset, offset + adjustedText.length() - 1), comment.isEmpty()));
 			}
 
 			@Override
@@ -327,9 +328,9 @@
 					throw new DocumentValidationException(result.getMessage());
 				}
 
-				fireBeforeContentInserted(new ContentChangeEvent(Document.this, pi.getParent(), new ContentRange(offset, offset + adjustedText.length() - 1), false));
+				fireBeforeContentInserted(new ContentChangeEvent(Document.this, pi.getParent(), new ContentRange(offset, offset + adjustedText.length() - 1), pi.isEmpty()));
 				getContent().insertText(offset, adjustedText);
-				fireContentInserted(new ContentChangeEvent(Document.this, pi.getParent(), new ContentRange(offset, offset + adjustedText.length() - 1), false));
+				fireContentInserted(new ContentChangeEvent(Document.this, pi.getParent(), new ContentRange(offset, offset + adjustedText.length() - 1), pi.isEmpty()));
 			}
 
 			@Override
@@ -339,16 +340,78 @@
 		});
 	}
 
-	private String convertControlCharactersToSpaces(final String text) {
+	private static String convertControlCharactersToSpaces(final String text) {
 		final char[] characters = text.toCharArray();
 		for (int i = 0; i < characters.length; i++) {
-			if (Character.isISOControl(characters[i]) && characters[i] != '\n') {
+			if (Character.isISOControl(characters[i]) && characters[i] != '\n' && characters[i] != '\t') {
 				characters[i] = ' ';
 			}
 		}
 		return new String(characters);
 	}
 
+	@Override
+	public void insertLineBreak(final int offset) throws DocumentValidationException {
+		Assert.isTrue(offset > getStartOffset() && offset <= getEndOffset(), MessageFormat.format("Offset must be in [{0}, {1}]", getStartOffset() + 1, getEndOffset()));
+
+		final INode insertionNode = getNodeForInsertionAt(offset);
+		insertionNode.accept(new INodeVisitor() {
+			@Override
+			public void visit(final IDocument document) {
+				Assert.isTrue(false, "Cannot insert a line break directly into Document.");
+			}
+
+			@Override
+			public void visit(final IDocumentFragment fragment) {
+				Assert.isTrue(false, "DocumentFragment is never a child of Document.");
+			}
+
+			@Override
+			public void visit(final IElement element) {
+				if (!canInsertAt(element, offset, IValidator.PCDATA)) {
+					throw new DocumentValidationException(MessageFormat.format("Cannot insert a line break into a {0} element at offset {1}.", element.getLocalName(), offset));
+				}
+				insertLineBreak(offset, element);
+			}
+
+			@Override
+			public void visit(final IText text) {
+				insertLineBreak(offset, text.getParent());
+			}
+
+			@Override
+			public void visit(final IComment comment) {
+				insertLineBreak(offset, comment.getParent());
+			}
+
+			@Override
+			public void visit(final IProcessingInstruction pi) {
+				// The target is validated to ensure the instruction is valid after the insertion
+				final String charBefore = pi.getText(new ContentRange(offset - 1, offset - 1));
+				final String charAfter = pi.getText(new ContentRange(offset, offset));
+				final String candidate = charBefore + '\n' + charAfter; // TODO '\n' is an implementation detail that we should not have to rely on at this point!
+
+				final IValidationResult result = XML.validateProcessingInstructionData(candidate);
+				if (!result.isOK()) {
+					throw new DocumentValidationException(result.getMessage());
+				}
+
+				insertLineBreak(offset, pi.getParent());
+			}
+
+			private void insertLineBreak(final int offset, final IParent parent) {
+				fireBeforeContentInserted(new ContentChangeEvent(Document.this, parent, new ContentRange(offset, offset), true));
+				getContent().insertLineBreak(offset);
+				fireContentInserted(new ContentChangeEvent(Document.this, parent, new ContentRange(offset, offset), true));
+			}
+
+			@Override
+			public void visit(final IIncludeNode document) {
+				Assert.isTrue(false, "Cannot insert text into an Include.");
+			}
+		});
+	}
+
 	/**
 	 * Inserts a node at the given offset. There is no check that the insertion is valid.
 	 *
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/dom/GapContent.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/dom/GapContent.java
index 30ccb50..dfc23f6 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/dom/GapContent.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/dom/GapContent.java
@@ -13,7 +13,9 @@
  *******************************************************************************/
 package org.eclipse.vex.core.internal.dom;
 
+import java.util.ArrayList;
 import java.util.Iterator;
+import java.util.List;
 import java.util.SortedSet;
 import java.util.TreeSet;
 
@@ -21,6 +23,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;
 
 /**
  * Implementation of the <code>Content</code> interface that manages changes efficiently. Implements a buffer that keeps
@@ -35,6 +38,7 @@
 	private static final float GROWTH_RATE_SLOW = 1.1f;
 
 	private static final char TAG_MARKER = '\0';
+	private static final char LINE_BREAK = '\n';
 
 	private char[] content;
 	private int gapStart;
@@ -73,7 +77,7 @@
 
 	@Override
 	public void removePosition(final IPosition position) {
-		if (positions.contains(position)) {
+		if (position.isValid() && positions.contains(position)) {
 			/*
 			 * This cast is save: if the position can be removed, this instance must have created it, hence it is a
 			 * GapContentPosition.
@@ -150,6 +154,18 @@
 		return c == TAG_MARKER;
 	}
 
+	public boolean isLineBreak(final int offset) {
+		if (offset < 0 || offset >= length()) {
+			return false;
+		}
+
+		return isLineBreak(content[getIndex(offset)]);
+	}
+
+	private boolean isLineBreak(final char c) {
+		return c == LINE_BREAK;
+	}
+
 	@Override
 	public void remove(final ContentRange range) {
 		assertOffset(range.getStartOffset(), 0, length() - range.length());
@@ -180,20 +196,30 @@
 	public String getText(final ContentRange range) {
 		Assert.isTrue(getRange().contains(range));
 
-		final int delta = gapEnd - gapStart;
+		final List<ContentRange> affectedRanges = expandAroundGap(range);
+
 		// Use range length as initial capacity. This might be a bit too much, but that's better than having to resize the StringBuilder.
 		final StringBuilder result = new StringBuilder(range.length());
-		if (range.getEndOffset() < gapStart) {
-			appendPlainText(result, range);
-		} else if (range.getStartOffset() >= gapStart) {
-			appendPlainText(result, range.moveBy(delta));
-		} else {
-			appendPlainText(result, new ContentRange(range.getStartOffset(), gapStart - 1));
-			appendPlainText(result, new ContentRange(gapEnd, range.getEndOffset() + delta));
+		for (final ContentRange affectedRange : affectedRanges) {
+			appendPlainText(result, affectedRange);
 		}
 		return result.toString();
 	}
 
+	private List<ContentRange> expandAroundGap(final ContentRange range) {
+		final List<ContentRange> affectedRanges = new ArrayList<ContentRange>();
+		final int delta = gapEnd - gapStart;
+		if (range.getEndOffset() < gapStart) {
+			affectedRanges.add(range);
+		} else if (range.getStartOffset() >= gapStart) {
+			affectedRanges.add(range.moveBy(delta));
+		} else {
+			affectedRanges.add(new ContentRange(range.getStartOffset(), gapStart - 1));
+			affectedRanges.add(new ContentRange(gapEnd, range.getEndOffset() + delta));
+		}
+		return affectedRanges;
+	}
+
 	private void appendPlainText(final StringBuilder stringBuilder, final ContentRange range) {
 		final int endOffset = range.getEndOffset();
 		for (int i = range.getStartOffset(); i <= endOffset; i++) {
@@ -213,17 +239,14 @@
 	public String getRawText(final ContentRange range) {
 		Assert.isTrue(getRange().contains(range));
 
-		final int delta = gapEnd - gapStart;
-		if (range.getEndOffset() < gapStart) {
-			return new String(content, range.getStartOffset(), range.length());
-		} else if (range.getStartOffset() >= gapStart) {
-			return new String(content, range.getStartOffset() + delta, range.length());
-		} else {
-			final StringBuilder result = new StringBuilder(range.length());
-			appendRawText(result, new ContentRange(range.getStartOffset(), gapStart - 1));
-			appendRawText(result, new ContentRange(gapEnd, range.getEndOffset() + delta));
-			return result.toString();
+		final List<ContentRange> affectedRanges = expandAroundGap(range);
+
+		// Use range length as initial capacity. This might be a bit too much, but that's better than having to resize the StringBuilder.
+		final StringBuilder result = new StringBuilder(range.length());
+		for (final ContentRange affectedRange : affectedRanges) {
+			appendRawText(result, affectedRange);
 		}
+		return result.toString();
 	}
 
 	private void appendRawText(final StringBuilder stringBuilder, final ContentRange range) {
@@ -231,6 +254,40 @@
 	}
 
 	@Override
+	public void insertLineBreak(final int offset) {
+		insertText(offset, Character.toString(LINE_BREAK));
+	}
+
+	@Override
+	public MultilineText getMultilineText(final ContentRange range) {
+		Assert.isTrue(getRange().contains(range));
+		final MultilineText result = new MultilineText();
+
+		StringBuilder currentLine = new StringBuilder();
+		int lineStart = range.getStartOffset();
+		for (int i = range.getStartOffset(); i <= range.getEndOffset(); i += 1) {
+			final char c = charAt(i);
+			if (isTagMarker(c)) {
+				// ignore tag markers
+			} else if (isLineBreak(c)) {
+				currentLine.append(c);
+				final ContentRange lineRange = new ContentRange(lineStart, i);
+				result.appendLine(currentLine.toString(), lineRange);
+				currentLine = new StringBuilder();
+				lineStart = i + 1;
+			} else {
+				currentLine.append(c);
+			}
+		}
+
+		if (currentLine.length() > 0) {
+			result.appendLine(currentLine.toString(), new ContentRange(lineStart, range.getEndOffset()));
+		}
+
+		return result;
+	}
+
+	@Override
 	public void insertContent(final int offset, final IContent content) {
 		assertOffset(offset, 0, length());
 
@@ -285,11 +342,7 @@
 	 */
 	@Override
 	public char charAt(final int offset) {
-		if (offset < gapStart) {
-			return content[offset];
-		} else {
-			return content[offset - gapStart + gapEnd];
-		}
+		return content[getIndex(offset)];
 	}
 
 	/**
@@ -304,7 +357,12 @@
 	 */
 	@Override
 	public CharSequence subSequence(final int startOffset, final int endOffset) {
-		return getRawText(new ContentRange(startOffset, endOffset));
+		Assert.isTrue(startOffset <= endOffset);
+		if (startOffset == endOffset) {
+			return "";
+		}
+
+		return getRawText(new ContentRange(startOffset, endOffset - 1));
 	}
 
 	/*
@@ -447,4 +505,9 @@
 		}
 	}
 
+	@Override
+	public String toString() {
+		return getRawText();
+	}
+
 }
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/io/UniversalTestDocument.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/io/UniversalTestDocument.java
new file mode 100644
index 0000000..4529a2b
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/io/UniversalTestDocument.java
@@ -0,0 +1,226 @@
+/*******************************************************************************
+ * 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.io;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+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.provisional.dom.AttributeDefinition;
+import org.eclipse.vex.core.provisional.dom.DocumentContentModel;
+import org.eclipse.vex.core.provisional.dom.IAttribute;
+import org.eclipse.vex.core.provisional.dom.IDocument;
+import org.eclipse.vex.core.provisional.dom.IElement;
+import org.eclipse.vex.core.provisional.dom.IParent;
+import org.eclipse.vex.core.provisional.dom.IValidator;
+
+/**
+ * @author Florian Thienel
+ */
+public class UniversalTestDocument {
+
+	public static final QualifiedName DOC = new QualifiedName(null, "doc");
+	public static final QualifiedName SECTION = new QualifiedName(null, "section");
+	public static final QualifiedName PARA = new QualifiedName(null, "para");
+	public static final QualifiedName ANCHOR = new QualifiedName(null, "anchor");
+	public static final QualifiedName B = new QualifiedName(null, "b");
+	public static final QualifiedName I = new QualifiedName(null, "i");
+
+	private static final String LOREM_IPSUM_LONG = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur.";
+
+	private final IDocument document;
+
+	public UniversalTestDocument(final int sampleCount) {
+		document = createSimpleTestDocument(sampleCount);
+	}
+
+	public IDocument getDocument() {
+		return document;
+	}
+
+	public IElement getSection(final int index) {
+		return (IElement) document.getRootElement().children().get(index);
+	}
+
+	public IElement getParagraphWithText(final int index) {
+		return (IElement) getSection(index).children().first();
+	}
+
+	public IElement getEmptyParagraph(final int index) {
+		return (IElement) getSection(index).children().last();
+	}
+
+	public int getOffsetWithinText(final int index) {
+		final IElement paragraph = getParagraphWithText(index);
+		return paragraph.getStartOffset() + 5;
+	}
+
+	public static IDocument createSimpleTestDocument(final int sampleCount) {
+		final Document document = new Document(DOC);
+		document.setValidator(new UniversalTestDocumentValidator());
+
+		for (int i = 0; i < sampleCount; i += 1) {
+			insertSection(document.getRootElement(), i, false);
+		}
+		return document;
+	}
+
+	private static void insertSection(final IParent parent, final int index, final boolean withInlineElements) {
+		final IElement section = insertElement(parent, SECTION);
+		insertParagraph(section, index, withInlineElements);
+		insertEmptyParagraph(section);
+	}
+
+	private static void insertParagraph(final IParent parent, final int index, final boolean withInlineElements) {
+		if (withInlineElements) {
+			insertTextWithInlineElements(insertEmptyParagraph(parent), index + " " + LOREM_IPSUM_LONG);
+		} else {
+			insertText(insertEmptyParagraph(parent), index + " " + LOREM_IPSUM_LONG);
+		}
+	}
+
+	private static IElement insertEmptyParagraph(final IParent parent) {
+		return insertElement(parent, PARA);
+	}
+
+	private static IElement insertElement(final IParent parent, final QualifiedName elementName) {
+		final IDocument document = parent.getDocument();
+		return document.insertElement(parent.getEndOffset(), elementName);
+	}
+
+	private static void insertText(final IParent parent, final String text) {
+		final IDocument document = parent.getDocument();
+		document.insertText(parent.getEndOffset(), text);
+	}
+
+	public static IDocument createTestDocumentWithAllFeatures(final int sampleCount) {
+		final Document document = new Document(DOC);
+		document.setValidator(new UniversalTestDocumentValidator());
+
+		for (int i = 0; i < sampleCount; i += 1) {
+			insertSection(document.getRootElement(), i, true);
+		}
+		return document;
+	}
+
+	private static void insertTextWithInlineElements(final IParent parent, final String text) {
+		final IDocument document = parent.getDocument();
+
+		int startOffset = 0;
+		while (startOffset < text.length()) {
+			final int wordStart = text.indexOf(" ", startOffset);
+			final int wordEnd = text.indexOf(" ", wordStart + 1);
+
+			if (wordStart < 0 || wordEnd < 0) {
+				document.insertText(parent.getEndOffset(), text.substring(startOffset));
+				startOffset = text.length();
+			} else {
+				document.insertText(parent.getEndOffset(), text.substring(startOffset, wordStart + 1));
+				final IElement inlineElement = document.insertElement(parent.getEndOffset(), B);
+				document.insertText(inlineElement.getEndOffset(), text.substring(wordStart + 1, wordEnd));
+				document.insertText(parent.getEndOffset(), text.substring(wordEnd, wordEnd + 1));
+				startOffset = wordEnd + 1;
+			}
+		}
+	}
+
+	private static class UniversalTestDocumentValidator implements IValidator {
+
+		private static final Map<QualifiedName, Set<QualifiedName>> VALID_ITEMS = new HashMap<QualifiedName, Set<QualifiedName>>() {
+			private static final long serialVersionUID = 1L;
+
+			{
+				put(DOC, set(SECTION));
+				put(SECTION, set(PARA, ANCHOR));
+				put(PARA, set(IValidator.PCDATA, B, I, ANCHOR));
+				put(ANCHOR, set());
+				put(B, set(IValidator.PCDATA, B, I, ANCHOR));
+				put(I, set(IValidator.PCDATA, B, I, ANCHOR));
+			}
+		};
+
+		private final DocumentContentModel documentContentModel = new DocumentContentModel();
+
+		@Override
+		public DocumentContentModel getDocumentContentModel() {
+			return documentContentModel;
+		}
+
+		@Override
+		public AttributeDefinition getAttributeDefinition(final IAttribute attribute) {
+			throw new UnsupportedOperationException();
+		}
+
+		@Override
+		public List<AttributeDefinition> getAttributeDefinitions(final IElement element) {
+			return Collections.emptyList();
+		}
+
+		@Override
+		public Set<QualifiedName> getValidItems(final IElement element) {
+			return VALID_ITEMS.get(element.getQualifiedName());
+		}
+
+		private static Set<QualifiedName> set(final QualifiedName... names) {
+			return new HashSet<QualifiedName>(Arrays.asList(names));
+		}
+
+		@Override
+		public boolean isValidSequence(final QualifiedName element, final List<QualifiedName> nodes, final boolean partial) {
+			final Set<QualifiedName> validItems = VALID_ITEMS.get(element);
+			for (final QualifiedName node : nodes) {
+				if (!validItems.contains(node)) {
+					return false;
+				}
+			}
+			return true;
+		}
+
+		@Override
+		public boolean isValidSequence(final QualifiedName element, final List<QualifiedName> seq1, final List<QualifiedName> seq2, final List<QualifiedName> seq3, final boolean partial) {
+			final List<QualifiedName> joinedSequence = new ArrayList<QualifiedName>();
+			if (seq1 != null) {
+				joinedSequence.addAll(seq1);
+			}
+			if (seq2 != null) {
+				joinedSequence.addAll(seq2);
+			}
+			if (seq3 != null) {
+				joinedSequence.addAll(seq3);
+			}
+			return isValidSequence(element, joinedSequence, partial);
+		}
+
+		@Override
+		public boolean isValidSequenceXInclude(final List<QualifiedName> nodes, final boolean partial) {
+			return true;
+		}
+
+		@Override
+		public Set<QualifiedName> getValidRootElements() {
+			return set(DOC);
+		}
+
+		@Override
+		public Set<String> getRequiredNamespaces() {
+			return Collections.emptySet();
+		}
+
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/AbstractBlockBox.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/AbstractBlockBox.java
index 55dc48b..719acff 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/AbstractBlockBox.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/AbstractBlockBox.java
@@ -22,6 +22,7 @@
 import org.eclipse.vex.core.internal.core.FontMetrics;
 import org.eclipse.vex.core.internal.core.Graphics;
 import org.eclipse.vex.core.internal.core.Insets;
+import org.eclipse.vex.core.internal.core.LineStyle;
 import org.eclipse.vex.core.internal.css.StyleSheet;
 import org.eclipse.vex.core.internal.css.Styles;
 import org.eclipse.vex.core.provisional.dom.BaseNodeVisitorWithResult;
@@ -573,13 +574,13 @@
 			foreground = g.getSystemColor(ColorResource.SELECTION_FOREGROUND);
 			background = g.getSystemColor(ColorResource.SELECTION_BACKGROUND);
 		} else {
-			foreground = g.createColor(new Color(0, 0, 0));
-			background = g.createColor(new Color(0xcc, 0xcc, 0xcc));
+			foreground = g.getColor(new Color(0, 0, 0));
+			background = g.getColor(new Color(0xcc, 0xcc, 0xcc));
 		}
 
 		final FontMetrics fm = g.getFontMetrics();
 		final ColorResource oldColor = g.setColor(background);
-		g.setLineStyle(Graphics.LINE_SOLID);
+		g.setLineStyle(LineStyle.SOLID);
 		g.setLineWidth(1);
 		final String frameName = getSelectionFrameName(node);
 		final int tabWidth = g.stringWidth(frameName) + fm.getLeading();
@@ -592,10 +593,6 @@
 		g.drawString(frameName, tabX + fm.getLeading() / 2, tabY);
 
 		g.setColor(oldColor);
-		if (!selected) {
-			foreground.dispose();
-			background.dispose();
-		}
 	}
 
 	protected String getSelectionFrameName(final INode node) {
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/AbstractBox.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/AbstractBox.java
index 9b3833a..8823562 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/AbstractBox.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/AbstractBox.java
@@ -16,6 +16,7 @@
 import org.eclipse.vex.core.internal.core.ColorResource;
 import org.eclipse.vex.core.internal.core.Graphics;
 import org.eclipse.vex.core.internal.core.Insets;
+import org.eclipse.vex.core.internal.core.LineStyle;
 import org.eclipse.vex.core.internal.core.Rectangle;
 import org.eclipse.vex.core.internal.css.CSS;
 import org.eclipse.vex.core.internal.css.Styles;
@@ -336,11 +337,10 @@
 		final Color backgroundColor = styles.getBackgroundColor();
 
 		if (backgroundColor != null) {
-			final ColorResource color = g.createColor(backgroundColor);
+			final ColorResource color = g.getColor(backgroundColor);
 			final ColorResource oldColor = g.setColor(color);
 			g.fillRect(left, top, right - left, bottom - top);
 			g.setColor(oldColor);
-			color.dispose();
 		}
 
 		if (drawBorders) {
@@ -360,46 +360,42 @@
 
 			// Bottom border
 			if (styles.getBorderBottomWidth() > 0) {
-				final ColorResource color = g.createColor(styles.getBorderBottomColor());
+				final ColorResource color = g.getColor(styles.getBorderBottomColor());
 				final ColorResource oldColor = g.setColor(color);
 				g.setLineStyle(lineStyle(styles.getBorderBottomStyle()));
 				g.setLineWidth(styles.getBorderBottomWidth());
 				g.drawLine(left + bw2, bottom - bw2 - 1, right - bw2, bottom - bw2 - 1);
 				g.setColor(oldColor);
-				color.dispose();
 			}
 
 			// Left border
 			if (hasLeft && styles.getBorderLeftWidth() > 0) {
-				final ColorResource color = g.createColor(styles.getBorderLeftColor());
+				final ColorResource color = g.getColor(styles.getBorderLeftColor());
 				final ColorResource oldColor = g.setColor(color);
 				g.setLineStyle(lineStyle(styles.getBorderLeftStyle()));
 				g.setLineWidth(styles.getBorderLeftWidth());
 				g.drawLine(left + lw2, top + lw2, left + lw2, bottom - lw2 - 1);
 				g.setColor(oldColor);
-				color.dispose();
 			}
 
 			// Right border
 			if (hasRight && styles.getBorderRightWidth() > 0) {
-				final ColorResource color = g.createColor(styles.getBorderRightColor());
+				final ColorResource color = g.getColor(styles.getBorderRightColor());
 				final ColorResource oldColor = g.setColor(color);
 				g.setLineStyle(lineStyle(styles.getBorderRightStyle()));
 				g.setLineWidth(styles.getBorderRightWidth());
 				g.drawLine(right - rw2 - 1, top + rw2, right - rw2 - 1, bottom - rw2 - 1);
 				g.setColor(oldColor);
-				color.dispose();
 			}
 
 			// Top border
 			if (styles.getBorderTopWidth() > 0) {
-				final ColorResource color = g.createColor(styles.getBorderTopColor());
+				final ColorResource color = g.getColor(styles.getBorderTopColor());
 				final ColorResource oldColor = g.setColor(color);
 				g.setLineStyle(lineStyle(styles.getBorderTopStyle()));
 				g.setLineWidth(styles.getBorderTopWidth());
 				g.drawLine(left + tw2, top + tw2, right - tw2, top + tw2);
 				g.setColor(oldColor);
-				color.dispose();
 			}
 
 			// g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
@@ -412,15 +408,14 @@
 	/**
 	 * Convert a CSS line style string (e.g. "dotted") to the corresponding Graphics.LINE_XXX style.
 	 */
-	private static int lineStyle(final String style) {
-		if (style.equals(CSS.DOTTED)) {
-			return Graphics.LINE_DOT;
-		} else if (style.equals(CSS.DASHED)) {
-			return Graphics.LINE_DASH;
+	private static LineStyle lineStyle(final String style) {
+		if (CSS.DOTTED.equals(style)) {
+			return LineStyle.DOTTED;
+		} else if (CSS.DASHED.equals(style)) {
+			return LineStyle.DASHED;
 		} else {
-			return Graphics.LINE_SOLID;
+			return LineStyle.SOLID;
 		}
-
 	}
 
 	/**
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/CircleBullet.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/CircleBullet.java
index 99b3779..99a08e5 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/CircleBullet.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/CircleBullet.java
@@ -11,6 +11,7 @@
 package org.eclipse.vex.core.internal.layout;
 
 import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.LineStyle;
 import org.eclipse.vex.core.internal.core.Rectangle;
 
 /**
@@ -24,7 +25,7 @@
 
 	@Override
 	public void draw(final Graphics g, final int x, final int y) {
-		g.setLineStyle(Graphics.LINE_SOLID);
+		g.setLineStyle(LineStyle.SOLID);
 		g.setLineWidth(1);
 		g.drawOval(x, y - size - lift, size, size);
 	}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/CompositeInlineBox.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/CompositeInlineBox.java
index 8a19272..fc004c6 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/CompositeInlineBox.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/CompositeInlineBox.java
@@ -92,12 +92,11 @@
 		final Graphics g = context.getGraphics();
 		final Styles styles = context.getStyleSheet().getStyles(getNode());
 
-		final FontResource font = g.createFont(styles.getFont());
-		final FontResource oldFont = g.setFont(font);
+		final FontResource font = g.getFont(styles.getFont());
+		final FontResource oldFont = g.setCurrentFont(font);
 		final FontMetrics fm = g.getFontMetrics();
 		final int height = fm.getAscent() + fm.getDescent();
-		g.setFont(oldFont);
-		font.dispose();
+		g.setCurrentFont(oldFont);
 
 		final int lineHeight = styles.getLineHeight();
 		final int y = (lineHeight - height) / 2;
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/DocumentTextBox.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/DocumentTextBox.java
index 3e3cb2f..348167f 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/DocumentTextBox.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/DocumentTextBox.java
@@ -141,13 +141,10 @@
 		final Styles styles = context.getStyleSheet().getStyles(getNode());
 		final Graphics g = context.getGraphics();
 
-		final FontResource font = g.createFont(styles.getFont());
-		final FontResource oldFont = g.setFont(font);
-		final ColorResource foreground = g.createColor(styles.getColor());
+		final FontResource font = g.getFont(styles.getFont());
+		final FontResource oldFont = g.setCurrentFont(font);
+		final ColorResource foreground = g.getColor(styles.getColor());
 		final ColorResource oldForeground = g.setColor(foreground);
-		// ColorResource background =
-		// g.createColor(styles.getBackgroundColor());
-		// ColorResource oldBackground = g.setBackgroundColor(background);
 
 		final char[] chars = getText().toCharArray();
 
@@ -192,12 +189,8 @@
 			paintTextDecoration(context, styles, s, x1, y);
 		}
 
-		g.setFont(oldFont);
+		g.setCurrentFont(oldFont);
 		g.setColor(oldForeground);
-		// g.setBackgroundColor(oldBackground);
-		font.dispose();
-		foreground.dispose();
-		// background.dispose();
 	}
 
 	/**
@@ -245,8 +238,8 @@
 
 		final Graphics g = context.getGraphics();
 		final Styles styles = context.getStyleSheet().getStyles(getNode());
-		final FontResource font = g.createFont(styles.getFont());
-		final FontResource oldFont = g.setFont(font);
+		final FontResource font = g.getFont(styles.getFont());
+		final FontResource oldFont = g.setCurrentFont(font);
 		final char[] chars = getText().toCharArray();
 
 		if (getWidth() <= 0) {
@@ -280,8 +273,7 @@
 			offset++;
 		}
 
-		g.setFont(oldFont);
-		font.dispose();
+		g.setCurrentFont(oldFont);
 		return new ContentPosition(getNode(), getStartOffset() + offset);
 	}
 
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/DrawableBox.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/DrawableBox.java
index 1c1490d..a20901d 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/DrawableBox.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/DrawableBox.java
@@ -103,10 +103,10 @@
 			drawSelected = getNode().getEndOffset() >= context.getSelectionStart() && getNode().getEndOffset() + 1 <= context.getSelectionEnd();
 		}
 
-		final FontResource font = g.createFont(styles.getFont());
-		final ColorResource color = g.createColor(styles.getColor());
+		final FontResource font = g.getFont(styles.getFont());
+		final ColorResource color = g.getColor(styles.getColor());
 
-		final FontResource oldFont = g.setFont(font);
+		final FontResource oldFont = g.setCurrentFont(font);
 		final ColorResource oldColor = g.setColor(color);
 
 		final FontMetrics fm = g.getFontMetrics();
@@ -120,10 +120,8 @@
 
 		drawable.draw(g, x, y);
 
-		g.setFont(oldFont);
+		g.setCurrentFont(oldFont);
 		g.setColor(oldColor);
-		font.dispose();
-		color.dispose();
 	}
 
 	/**
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/HCaret.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/HCaret.java
index e6f06ff..9710c4a 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/HCaret.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/HCaret.java
@@ -40,11 +40,10 @@
 
 	@Override
 	public void draw(final Graphics g, final Color color) {
-		final ColorResource newColor = g.createColor(color);
+		final ColorResource newColor = g.getColor(color);
 		final ColorResource oldColor = g.setColor(newColor);
 		g.fillRect(getX(), getY(), length, LINE_WIDTH);
 		g.setColor(oldColor);
-		newColor.dispose();
 	}
 
 	/**
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/IncludeInlineBox.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/IncludeInlineBox.java
index 7501599..b055987 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/IncludeInlineBox.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/IncludeInlineBox.java
@@ -113,14 +113,13 @@
 	private void layout(final LayoutContext context) {
 		final Graphics g = context.getGraphics();
 		final Styles styles = context.getStyleSheet().getStyles(node);
-		final FontResource font = g.createFont(styles.getFont());
-		final FontResource oldFont = g.setFont(font);
+		final FontResource font = g.getFont(styles.getFont());
+		final FontResource oldFont = g.setCurrentFont(font);
 		final FontMetrics fm = g.getFontMetrics();
 		setHeight(styles.getLineHeight());
 		final int halfLeading = (styles.getLineHeight() - fm.getAscent() - fm.getDescent()) / 2;
 		baseline = halfLeading + fm.getAscent();
-		g.setFont(oldFont);
-		font.dispose();
+		g.setCurrentFont(oldFont);
 
 		int x = 0;
 		for (final InlineBox child : children) {
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/InlineElementBox.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/InlineElementBox.java
index 985fb99..1b0f708 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/InlineElementBox.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/InlineElementBox.java
@@ -19,6 +19,7 @@
 import org.eclipse.vex.core.internal.core.FontMetrics;
 import org.eclipse.vex.core.internal.core.FontResource;
 import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.LineStyle;
 import org.eclipse.vex.core.internal.core.Rectangle;
 import org.eclipse.vex.core.internal.css.CSS;
 import org.eclipse.vex.core.internal.css.Styles;
@@ -386,7 +387,7 @@
 		final Drawable drawable = new Drawable() {
 			@Override
 			public void draw(final Graphics g, final int x, int y) {
-				g.setLineStyle(Graphics.LINE_SOLID);
+				g.setLineStyle(LineStyle.SOLID);
 				g.setLineWidth(1);
 				y -= lift;
 				g.drawLine(x, y - size, x, y);
@@ -408,7 +409,7 @@
 		final Drawable drawable = new Drawable() {
 			@Override
 			public void draw(final Graphics g, final int x, int y) {
-				g.setLineStyle(Graphics.LINE_SOLID);
+				g.setLineStyle(LineStyle.SOLID);
 				g.setLineWidth(1);
 				y -= lift;
 				g.drawLine(x + size - 1, y - size, x + size - 1, y);
@@ -427,14 +428,13 @@
 	private void layout(final LayoutContext context) {
 		final Graphics g = context.getGraphics();
 		final Styles styles = context.getStyleSheet().getStyles(node);
-		final FontResource font = g.createFont(styles.getFont());
-		final FontResource oldFont = g.setFont(font);
+		final FontResource font = g.getFont(styles.getFont());
+		final FontResource oldFont = g.setCurrentFont(font);
 		final FontMetrics fm = g.getFontMetrics();
 		setHeight(styles.getLineHeight());
 		halfLeading = (styles.getLineHeight() - fm.getAscent() - fm.getDescent()) / 2;
 		baseline = halfLeading + fm.getAscent();
-		g.setFont(oldFont);
-		font.dispose();
+		g.setCurrentFont(oldFont);
 
 		int x = 0;
 		for (final InlineBox child : children) {
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/LayoutUtils.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/LayoutUtils.java
index 49a24db..c578a65 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/LayoutUtils.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/LayoutUtils.java
@@ -77,12 +77,7 @@
 	 *            The node passed to Styles#getContent (to get attr values from)
 	 */
 	public static String getGeneratedContent(final LayoutContext context, final Styles styles, final INode node) {
-		final List<String> content = styles.getContent(node);
-		final StringBuffer sb = new StringBuffer();
-		for (final String string : content) {
-			sb.append(string); // TODO: change to ContentPart
-		}
-		return sb.toString();
+		return styles.getTextualContent();
 	}
 
 	/**
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/PlaceholderBox.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/PlaceholderBox.java
index 8581c82..6592ad0 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/PlaceholderBox.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/PlaceholderBox.java
@@ -48,8 +48,8 @@
 
 		final Graphics g = context.getGraphics();
 		final Styles styles = context.getStyleSheet().getStyles(node);
-		final FontResource font = g.createFont(styles.getFont());
-		final FontResource oldFont = g.setFont(font);
+		final FontResource font = g.getFont(styles.getFont());
+		final FontResource oldFont = g.setCurrentFont(font);
 		final FontMetrics fm = g.getFontMetrics();
 		final int height = fm.getAscent() + fm.getDescent();
 
@@ -58,8 +58,7 @@
 
 		baseline = textTop + fm.getAscent();
 		setHeight(lineHeight);
-		g.setFont(oldFont);
-		font.dispose();
+		g.setCurrentFont(oldFont);
 	}
 
 	/**
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/SquareBullet.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/SquareBullet.java
index ff83e22..d6ff402 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/SquareBullet.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/SquareBullet.java
@@ -11,6 +11,7 @@
 package org.eclipse.vex.core.internal.layout;
 
 import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.LineStyle;
 import org.eclipse.vex.core.internal.core.Rectangle;
 
 /**
@@ -24,7 +25,7 @@
 
 	@Override
 	public void draw(final Graphics g, final int x, final int y) {
-		g.setLineStyle(Graphics.LINE_SOLID);
+		g.setLineStyle(LineStyle.SOLID);
 		g.setLineWidth(1);
 		g.drawRect(x, y - size - lift, size, size);
 	}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/StaticTextBox.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/StaticTextBox.java
index 2f62f83..4239496 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/StaticTextBox.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/StaticTextBox.java
@@ -135,10 +135,10 @@
 			drawSelected = getNode().getEndOffset() >= context.getSelectionStart() && getNode().getEndOffset() + 1 <= context.getSelectionEnd();
 		}
 
-		final FontResource font = g.createFont(styles.getFont());
-		final ColorResource color = g.createColor(styles.getColor());
+		final FontResource font = g.getFont(styles.getFont());
+		final ColorResource color = g.getColor(styles.getColor());
 
-		final FontResource oldFont = g.setFont(font);
+		final FontResource oldFont = g.setCurrentFont(font);
 		final ColorResource oldColor = g.setColor(color);
 
 		if (drawSelected) {
@@ -148,10 +148,8 @@
 		}
 		paintTextDecoration(context, styles, getText(), x, y);
 
-		g.setFont(oldFont);
+		g.setCurrentFont(oldFont);
 		g.setColor(oldColor);
-		font.dispose();
-		color.dispose();
 	}
 
 	/**
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/TextBox.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/TextBox.java
index 35566ae..60ba0f1 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/TextBox.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/TextBox.java
@@ -17,6 +17,7 @@
 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.LineStyle;
 import org.eclipse.vex.core.internal.css.Styles;
 import org.eclipse.vex.core.provisional.dom.ContentPosition;
 import org.eclipse.vex.core.provisional.dom.INode;
@@ -88,14 +89,13 @@
 
 		final Graphics g = context.getGraphics();
 		final Styles styles = context.getStyleSheet().getStyles(getNode());
-		final FontResource font = g.createFont(styles.getFont());
-		final FontResource oldFont = g.setFont(font);
+		final FontResource font = g.getFont(styles.getFont());
+		final FontResource oldFont = g.setCurrentFont(font);
 		final FontMetrics fm = g.getFontMetrics();
 		setHeight(styles.getLineHeight());
 		final int halfLeading = (getHeight() - (fm.getAscent() + fm.getDescent())) / 2;
 		setBaseline(halfLeading + fm.getAscent());
-		g.setFont(oldFont);
-		font.dispose();
+		g.setCurrentFont(oldFont);
 	}
 
 	/**
@@ -120,13 +120,12 @@
 	public Caret getCaret(final LayoutContext context, final ContentPosition position) {
 		final Graphics g = context.getGraphics();
 		final Styles styles = context.getStyleSheet().getStyles(node);
-		final FontResource oldFont = g.getFont();
-		final FontResource font = g.createFont(styles.getFont());
-		g.setFont(font);
+		final FontResource oldFont = g.getCurrentFont();
+		final FontResource font = g.getFont(styles.getFont());
+		g.setCurrentFont(font);
 		final char[] chars = getText().toCharArray();
 		final int x = g.charsWidth(chars, 0, position.getOffset() - getStartOffset());
-		g.setFont(oldFont);
-		font.dispose();
+		g.setCurrentFont(oldFont);
 		return new TextCaret(x, 0, getHeight());
 	}
 
@@ -231,7 +230,7 @@
 		final FontMetrics fm = g.getFontMetrics();
 		final int width = g.stringWidth(s);
 		final int lineWidth = fm.getAscent() / 12;
-		g.setLineStyle(Graphics.LINE_SOLID);
+		g.setLineStyle(LineStyle.SOLID);
 		g.setLineWidth(lineWidth);
 		g.drawLine(x, y, x + width, y);
 	}
@@ -258,8 +257,8 @@
 		final Graphics g = context.getGraphics();
 		final Styles styles = context.getStyleSheet().getStyles(node);
 
-		final FontResource font = g.createFont(styles.getFont());
-		final FontResource oldFont = g.setFont(font);
+		final FontResource font = g.getFont(styles.getFont());
+		final FontResource oldFont = g.setCurrentFont(font);
 
 		int split = 0;
 		int next = 1;
@@ -311,8 +310,7 @@
 		final Pair splitPair = splitAt(context, split, splitWidth, maxWidth);
 
 		// The created font is used by the calculateSize() method, so we have to keep it till after the splitAt method call.
-		g.setFont(oldFont);
-		font.dispose();
+		g.setCurrentFont(oldFont);
 
 		return splitPair;
 	}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/TextCaret.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/TextCaret.java
index fc5d6c2..41075c9 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/TextCaret.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/layout/TextCaret.java
@@ -42,11 +42,10 @@
 
 	@Override
 	public void draw(final Graphics g, final Color color) {
-		final ColorResource newColor = g.createColor(color);
+		final ColorResource newColor = g.getColor(color);
 		final ColorResource oldColor = g.setColor(newColor);
 		g.fillRect(getX(), getY(), LINE_WIDTH, height);
 		g.setColor(oldColor);
-		newColor.dispose();
 	}
 
 	@Override
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/AbstractUndoableEdit.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/AbstractUndoableEdit.java
index 21d2371..67248c9 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/AbstractUndoableEdit.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/AbstractUndoableEdit.java
@@ -24,7 +24,7 @@
 	private boolean hasBeenDone = false;
 
 	@Override
-	public boolean combine(final IUndoableEdit edit) {
+	public final boolean combine(final IUndoableEdit edit) {
 		if (hasBeenDone) {
 			return performCombine(edit);
 		}
@@ -44,15 +44,15 @@
 	}
 
 	@Override
-	public void redo() throws CannotRedoException {
+	public final void redo() throws CannotApplyException {
 		if (!canRedo()) {
-			throw new CannotRedoException();
+			throw new CannotApplyException();
 		}
 
 		try {
 			performRedo();
 			hasBeenDone = true;
-		} catch (final CannotRedoException ex) {
+		} catch (final CannotApplyException ex) {
 			hasBeenDone = false;
 			throw ex;
 		}
@@ -61,12 +61,12 @@
 	/**
 	 * To be implemented by subclasses to perform the actual Redo action.
 	 *
-	 * @throws CannotRedoException
+	 * @throws CannotApplyException
 	 */
-	protected abstract void performRedo() throws CannotRedoException;
+	protected abstract void performRedo() throws CannotApplyException;
 
 	@Override
-	public void undo() throws CannotUndoException {
+	public final void undo() throws CannotUndoException {
 		if (!canUndo()) {
 			throw new CannotUndoException();
 		}
@@ -95,5 +95,4 @@
 	public boolean canRedo() {
 		return !hasBeenDone;
 	}
-
 }
\ No newline at end of file
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/CannotRedoException.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/CannotApplyException.java
similarity index 81%
rename from org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/CannotRedoException.java
rename to org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/CannotApplyException.java
index 278359b..0098ce5 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/CannotRedoException.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/CannotApplyException.java
@@ -13,14 +13,14 @@
 /**
  * Thrown when an IUndoableEdit cannot be undone.
  */
-public class CannotRedoException extends RuntimeException {
+public class CannotApplyException extends RuntimeException {
 
 	private static final long serialVersionUID = 1L;
 
 	/**
 	 * Class constructor.
 	 */
-	public CannotRedoException() {
+	public CannotApplyException() {
 	}
 
 	/**
@@ -29,7 +29,7 @@
 	 * @param message
 	 *            Message indicating the reason for the failure.
 	 */
-	public CannotRedoException(final String message) {
+	public CannotApplyException(final String message) {
 		super(message);
 	}
 
@@ -39,7 +39,7 @@
 	 * @param cause
 	 *            Root cause of the failure.
 	 */
-	public CannotRedoException(final Throwable cause) {
+	public CannotApplyException(final Throwable cause) {
 		super(cause);
 	}
 
@@ -51,7 +51,7 @@
 	 * @param cause
 	 *            Root cause of the failure.
 	 */
-	public CannotRedoException(final String message, final Throwable cause) {
+	public CannotApplyException(final String message, final Throwable cause) {
 		super(message, cause);
 	}
 
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/ChangeAttributeEdit.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/ChangeAttributeEdit.java
index 62636a0..e9148f6 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/ChangeAttributeEdit.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/ChangeAttributeEdit.java
@@ -44,12 +44,20 @@
 	}

 

 	@Override

-	protected void performRedo() throws CannotRedoException {

+	protected void performRedo() throws CannotApplyException {

 		try {

 			final IElement element = document.getElementForInsertionAt(offset);

 			element.setAttribute(attributeName, newValue);

 		} catch (final DocumentValidationException e) {

-			throw new CannotRedoException(e);

+			throw new CannotApplyException(e);

 		}

 	}

+

+	public int getOffsetBefore() {

+		return offset;

+	}

+

+	public int getOffsetAfter() {

+		return offset;

+	}

 }

diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/ChangeNamespaceEdit.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/ChangeNamespaceEdit.java
index 94ab92a..3dd4257 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/ChangeNamespaceEdit.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/ChangeNamespaceEdit.java
@@ -47,7 +47,7 @@
 	}

 

 	@Override

-	protected void performRedo() throws CannotRedoException {

+	protected void performRedo() throws CannotApplyException {

 		try {

 			final IElement element = document.getElementForInsertionAt(offset);

 			if (newUri == null) {

@@ -56,7 +56,15 @@
 				element.declareNamespace(prefix, newUri);

 			}

 		} catch (final DocumentValidationException e) {

-			throw new CannotRedoException(e);

+			throw new CannotApplyException(e);

 		}

 	}

+

+	public int getOffsetBefore() {

+		return offset;

+	}

+

+	public int getOffsetAfter() {

+		return offset;

+	}

 }

diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/CompoundEdit.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/CompoundEdit.java
index adf63da..5fd0e26 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/CompoundEdit.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/CompoundEdit.java
@@ -20,11 +20,7 @@
  */
 public class CompoundEdit extends AbstractUndoableEdit {
 
-	/**
-	 * Class constructor.
-	 */
-	public CompoundEdit() {
-	}
+	private final List<IUndoableEdit> edits = new ArrayList<IUndoableEdit>();
 
 	/**
 	 * Adds an edit to the list.
@@ -72,7 +68,17 @@
 		return true;
 	}
 
-	// ===================================================== PRIVATE
+	public int getOffsetBefore() {
+		if (edits.isEmpty()) {
+			return 0;
+		}
+		return edits.get(0).getOffsetBefore();
+	}
 
-	private final List<IUndoableEdit> edits = new ArrayList<IUndoableEdit>();
+	public int getOffsetAfter() {
+		if (edits.isEmpty()) {
+			return 0;
+		}
+		return edits.get(edits.size() - 1).getOffsetAfter();
+	}
 }
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/DefineOffsetEdit.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/DefineOffsetEdit.java
new file mode 100644
index 0000000..a276408
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/DefineOffsetEdit.java
@@ -0,0 +1,42 @@
+/*******************************************************************************
+ * 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.undo;
+
+public class DefineOffsetEdit extends AbstractUndoableEdit {
+
+	private final int offsetBefore;
+	private final int offsetAfter;
+
+	public DefineOffsetEdit(final int offsetBefore, final int offsetAfter) {
+		this.offsetBefore = offsetBefore;
+		this.offsetAfter = offsetAfter;
+	}
+
+	@Override
+	public int getOffsetBefore() {
+		return offsetBefore;
+	}
+
+	@Override
+	public int getOffsetAfter() {
+		return offsetAfter;
+	}
+
+	@Override
+	protected void performRedo() throws CannotApplyException {
+		// ignore
+	}
+
+	@Override
+	protected void performUndo() throws CannotUndoException {
+		// ignore
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/DeleteEdit.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/DeleteEdit.java
index f55ffe0..30d3c4e 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/DeleteEdit.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/DeleteEdit.java
@@ -20,12 +20,14 @@
 

 	private final IDocument document;

 	private final ContentRange range;

+	private final int offsetToRestore;

 	private IDocumentFragment fragment = null;

 

-	public DeleteEdit(final IDocument document, final ContentRange range) {

+	public DeleteEdit(final IDocument document, final ContentRange range, final int offsetToRestore) {

 		super();

 		this.document = document;

 		this.range = range;

+		this.offsetToRestore = offsetToRestore;

 	}

 

 	@Override

@@ -39,13 +41,20 @@
 	}

 

 	@Override

-	protected void performRedo() throws CannotRedoException {

+	protected void performRedo() throws CannotApplyException {

 		try {

 			fragment = document.getFragment(range);

 			document.delete(range);

 		} catch (final DocumentValidationException e) {

-			throw new CannotRedoException(e);

+			throw new CannotApplyException(e);

 		}

 	}

 

+	public int getOffsetBefore() {

+		return offsetToRestore;

+	}

+

+	public int getOffsetAfter() {

+		return range.getStartOffset();

+	}

 }

diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/DeleteNextCharEdit.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/DeleteNextCharEdit.java
new file mode 100644
index 0000000..e388eab
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/DeleteNextCharEdit.java
@@ -0,0 +1,82 @@
+/*******************************************************************************
+ * 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 org.eclipse.vex.core.provisional.dom.ContentRange;
+import org.eclipse.vex.core.provisional.dom.DocumentValidationException;
+import org.eclipse.vex.core.provisional.dom.IDocument;
+
+/**
+ * @author Florian Thienel
+ */
+public class DeleteNextCharEdit extends AbstractUndoableEdit {
+
+	private final IDocument document;
+	private final int offset;
+
+	private int count;
+	private String textToRestore = null;
+
+	public DeleteNextCharEdit(final IDocument document, final int offset) {
+		this.document = document;
+		this.offset = offset;
+		count = 1;
+	}
+
+	@Override
+	protected boolean performCombine(final IUndoableEdit other) {
+		if (other instanceof DeleteNextCharEdit) {
+			final DeleteNextCharEdit otherEdit = (DeleteNextCharEdit) other;
+			if (otherEdit.offset == offset) {
+				count += otherEdit.count;
+				textToRestore += otherEdit.textToRestore;
+				return true;
+			}
+		}
+		return false;
+	}
+
+	@Override
+	protected void performRedo() throws CannotApplyException {
+		if (document.isTagAt(offset)) {
+			throw new CannotApplyException("Cannot delete a tag!");
+		}
+
+		try {
+			final ContentRange range = new ContentRange(offset, offset + count - 1);
+			textToRestore = document.getText(range);
+			document.delete(range);
+		} catch (final DocumentValidationException e) {
+			throw new CannotApplyException(e);
+		}
+	}
+
+	@Override
+	protected void performUndo() throws CannotUndoException {
+		try {
+			document.insertText(offset, textToRestore);
+			textToRestore = null;
+		} catch (final DocumentValidationException e) {
+			throw new CannotApplyException(e);
+		}
+	}
+
+	@Override
+	public int getOffsetBefore() {
+		return offset;
+	}
+
+	@Override
+	public int getOffsetAfter() {
+		return offset;
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/DeletePreviousCharEdit.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/DeletePreviousCharEdit.java
new file mode 100644
index 0000000..685ccc0
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/DeletePreviousCharEdit.java
@@ -0,0 +1,82 @@
+/*******************************************************************************
+ * 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 org.eclipse.vex.core.provisional.dom.ContentRange;
+import org.eclipse.vex.core.provisional.dom.DocumentValidationException;
+import org.eclipse.vex.core.provisional.dom.IDocument;
+
+/**
+ * @author Florian Thienel
+ */
+public class DeletePreviousCharEdit extends AbstractUndoableEdit {
+
+	private final IDocument document;
+	private final int offset;
+
+	private int count;
+	private String textToRestore = null;
+
+	public DeletePreviousCharEdit(final IDocument document, final int offset) {
+		this.document = document;
+		this.offset = offset;
+		count = 1;
+	}
+
+	@Override
+	protected boolean performCombine(final IUndoableEdit other) {
+		if (other instanceof DeletePreviousCharEdit) {
+			final DeletePreviousCharEdit otherEdit = (DeletePreviousCharEdit) other;
+			if (otherEdit.offset == offset - count) {
+				count += otherEdit.count;
+				textToRestore = otherEdit.textToRestore + textToRestore;
+				return true;
+			}
+		}
+		return false;
+	}
+
+	@Override
+	protected void performRedo() throws CannotApplyException {
+		if (document.isTagAt(offset - 1)) {
+			throw new CannotApplyException("Cannot delete a tag!");
+		}
+
+		try {
+			final ContentRange range = new ContentRange(offset - count, offset - 1);
+			textToRestore = document.getText(range);
+			document.delete(range);
+		} catch (final DocumentValidationException e) {
+			throw new CannotApplyException(e);
+		}
+	}
+
+	@Override
+	protected void performUndo() throws CannotUndoException {
+		try {
+			document.insertText(offset - count, textToRestore);
+			textToRestore = null;
+		} catch (final DocumentValidationException e) {
+			throw new CannotApplyException(e);
+		}
+	}
+
+	@Override
+	public int getOffsetBefore() {
+		return offset;
+	}
+
+	@Override
+	public int getOffsetAfter() {
+		return offset - count;
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/EditProcessingInstructionEdit.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/EditProcessingInstructionEdit.java
index 89c05b5..72dce52 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/EditProcessingInstructionEdit.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/EditProcessingInstructionEdit.java
@@ -49,16 +49,16 @@
 			document.insertText(pi.getEndOffset(), oldData);

 

 		} catch (final DocumentValidationException e) {

-			throw new CannotRedoException(e);

+			throw new CannotApplyException(e);

 		}

 	}

 

 	@Override

-	protected void performRedo() throws CannotRedoException {

+	protected void performRedo() throws CannotApplyException {

 		try {

 			final INode node = document.getNodeForInsertionAt(offset);

 			if (!(node instanceof IProcessingInstruction)) {

-				throw new CannotRedoException("Current Node is not a processing instruction.");

+				throw new CannotApplyException("Current Node is not a processing instruction.");

 			}

 

 			final IProcessingInstruction pi = (IProcessingInstruction) node;

@@ -70,14 +70,14 @@
 			if (target != null) {

 				final IValidationResult resultTarget = XML.validateProcessingInstructionTarget(target);

 				if (!resultTarget.isOK()) {

-					throw new CannotRedoException(resultTarget.getMessage());

+					throw new CannotApplyException(resultTarget.getMessage());

 				}

 			}

 

 			if (data != null) {

 				final IValidationResult resultData = XML.validateProcessingInstructionData(data);

 				if (!resultData.isOK()) {

-					throw new CannotRedoException(resultData.getMessage());

+					throw new CannotApplyException(resultData.getMessage());

 				}

 			}

 

@@ -91,7 +91,15 @@
 			}

 

 		} catch (final DocumentValidationException e) {

-			throw new CannotRedoException(e);

+			throw new CannotApplyException(e);

 		}

 	}

+

+	public int getOffsetBefore() {

+		return offset;

+	}

+

+	public int getOffsetAfter() {

+		return offset;

+	}

 }

diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/EditStack.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/EditStack.java
new file mode 100644
index 0000000..b373f1e
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/EditStack.java
@@ -0,0 +1,109 @@
+/*******************************************************************************
+ * 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 java.util.LinkedList;
+
+/**
+ * @author Florian Thienel
+ */
+public class EditStack {
+
+	private final LinkedList<IUndoableEdit> doneEdits = new LinkedList<IUndoableEdit>();
+	private final LinkedList<IUndoableEdit> undoneEdits = new LinkedList<IUndoableEdit>();
+
+	private final LinkedList<CompoundEdit> pendingEdits = new LinkedList<CompoundEdit>();
+
+	private IUndoableEdit cleanMarker = null;
+
+	public <T extends IUndoableEdit> T apply(final T edit) throws CannotApplyException {
+		edit.redo();
+
+		if (pendingEdits.isEmpty()) {
+			if (doneEdits.isEmpty() || !doneEdits.peek().combine(edit)) {
+				doneEdits.push(edit);
+			}
+			undoneEdits.clear();
+		} else {
+			pendingEdits.peek().addEdit(edit);
+		}
+
+		return edit;
+	}
+
+	public IUndoableEdit undo() throws CannotUndoException {
+		if (doneEdits.isEmpty()) {
+			throw new CannotUndoException("EditStack is empty, nothing to undo!");
+		}
+
+		final IUndoableEdit undoneEdit = doneEdits.peek();
+		undoneEdit.undo();
+
+		undoneEdits.push(doneEdits.pop());
+
+		return undoneEdit;
+	}
+
+	public IUndoableEdit redo() throws CannotApplyException {
+		if (undoneEdits.isEmpty()) {
+			throw new CannotApplyException("Nothing available to redo!");
+		}
+
+		final IUndoableEdit redoneEdit = undoneEdits.peek();
+		redoneEdit.redo();
+
+		doneEdits.push(undoneEdits.pop());
+
+		return redoneEdit;
+	}
+
+	public boolean canUndo() {
+		return !doneEdits.isEmpty() && doneEdits.peek().canUndo();
+	}
+
+	public boolean canRedo() {
+		return !undoneEdits.isEmpty() && undoneEdits.peek().canRedo();
+	}
+
+	public void beginWork() {
+		pendingEdits.push(new CompoundEdit());
+	}
+
+	public IUndoableEdit commitWork() {
+		if (pendingEdits.isEmpty()) {
+			throw new CannotApplyException("No edit pending, cannot commit!");
+		}
+
+		return apply(pendingEdits.pop());
+	}
+
+	public IUndoableEdit rollbackWork() {
+		if (pendingEdits.isEmpty()) {
+			throw new CannotUndoException("No edit pending, cannot rollback!");
+		}
+
+		final CompoundEdit work = pendingEdits.pop();
+		work.undo();
+		return work;
+	}
+
+	public boolean inTransaction() {
+		return !pendingEdits.isEmpty();
+	}
+
+	public boolean isDirty() {
+		return doneEdits.peek() != cleanMarker;
+	}
+
+	public void markClean() {
+		cleanMarker = doneEdits.peek();
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/IUndoableEdit.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/IUndoableEdit.java
index 65eece5..edb628e 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/IUndoableEdit.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/IUndoableEdit.java
@@ -31,7 +31,7 @@
 	/**
 	 * Redo the edit.
 	 */
-	public void redo() throws CannotRedoException;
+	public void redo() throws CannotApplyException;
 
 	/**
 	 * Undo the edit.
@@ -51,4 +51,8 @@
 	 * @return <code>true</code> to indicate that this edit can be redone, <code>false</code> otherwise.
 	 */
 	public boolean canRedo();
+
+	public int getOffsetBefore();
+
+	public int getOffsetAfter();
 }
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/InsertCommentEdit.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/InsertCommentEdit.java
index 6075194..2f38c61 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/InsertCommentEdit.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/InsertCommentEdit.java
@@ -43,12 +43,12 @@
 	}

 

 	@Override

-	protected void performRedo() throws CannotRedoException {

+	protected void performRedo() throws CannotApplyException {

 		try {

 			comment = document.insertComment(offset);

 			commentRange = comment.getRange();

 		} catch (final DocumentValidationException e) {

-			throw new CannotRedoException(e);

+			throw new CannotApplyException(e);

 		}

 	}

 

@@ -56,4 +56,11 @@
 		return comment;

 	}

 

+	public int getOffsetBefore() {

+		return offset;

+	}

+

+	public int getOffsetAfter() {

+		return comment.getEndOffset();

+	}

 }

diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/InsertElementEdit.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/InsertElementEdit.java
index d7c3404..a418f70 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/InsertElementEdit.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/InsertElementEdit.java
@@ -45,12 +45,12 @@
 	}

 

 	@Override

-	protected void performRedo() throws CannotRedoException {

+	protected void performRedo() throws CannotApplyException {

 		try {

 			element = document.insertElement(offset, elementName);

 			elementRange = element.getRange();

 		} catch (final DocumentValidationException e) {

-			throw new CannotRedoException(e);

+			throw new CannotApplyException(e);

 		}

 	}

 

@@ -58,4 +58,11 @@
 		return element;

 	}

 

+	public int getOffsetBefore() {

+		return offset;

+	}

+

+	public int getOffsetAfter() {

+		return element.getEndOffset();

+	}

 }

diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/InsertFragmentEdit.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/InsertFragmentEdit.java
index 798685d..fac6273 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/InsertFragmentEdit.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/InsertFragmentEdit.java
@@ -38,12 +38,19 @@
 	}

 

 	@Override

-	protected void performRedo() throws CannotRedoException {

+	protected void performRedo() throws CannotApplyException {

 		try {

 			document.insertFragment(offset, fragment);

 		} catch (final DocumentValidationException e) {

-			throw new CannotRedoException(e);

+			throw new CannotApplyException(e);

 		}

 	}

 

+	public int getOffsetBefore() {

+		return offset;

+	}

+

+	public int getOffsetAfter() {

+		return fragment.getContent().getRange().moveBy(offset).getEndOffset();

+	}

 }

diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/InsertLineBreakEdit.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/InsertLineBreakEdit.java
new file mode 100644
index 0000000..0b19691
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/InsertLineBreakEdit.java
@@ -0,0 +1,55 @@
+/*******************************************************************************
+ * 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 org.eclipse.vex.core.provisional.dom.ContentRange;
+import org.eclipse.vex.core.provisional.dom.DocumentValidationException;
+import org.eclipse.vex.core.provisional.dom.IDocument;
+
+/**
+ * @author Florian Thienel
+ */
+public class InsertLineBreakEdit extends AbstractUndoableEdit {
+
+	private final IDocument document;
+	private final int offset;
+
+	public InsertLineBreakEdit(final IDocument document, final int offset) {
+		this.document = document;
+		this.offset = offset;
+	}
+
+	@Override
+	protected void performRedo() throws CannotApplyException {
+		try {
+			document.insertLineBreak(offset);
+		} catch (final DocumentValidationException e) {
+			throw new CannotApplyException(e);
+		}
+	}
+
+	@Override
+	protected void performUndo() throws CannotUndoException {
+		try {
+			document.delete(new ContentRange(offset, offset));
+		} catch (final DocumentValidationException e) {
+			throw new CannotUndoException(e);
+		}
+	}
+
+	public int getOffsetBefore() {
+		return offset;
+	}
+
+	public int getOffsetAfter() {
+		return offset + 1;
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/InsertProcessingInstructionEdit.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/InsertProcessingInstructionEdit.java
index dda231c..6f5f5c6 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/InsertProcessingInstructionEdit.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/InsertProcessingInstructionEdit.java
@@ -44,16 +44,24 @@
 	}

 

 	@Override

-	protected void performRedo() throws CannotRedoException {

+	protected void performRedo() throws CannotApplyException {

 		try {

 			pi = document.insertProcessingInstruction(offset, target);

 			contentRange = pi.getRange();

 		} catch (final DocumentValidationException e) {

-			throw new CannotRedoException(e);

+			throw new CannotApplyException(e);

 		}

 	}

 

 	public IProcessingInstruction getProcessingInstruction() {

 		return pi;

 	}

+

+	public int getOffsetBefore() {

+		return offset;

+	}

+

+	public int getOffsetAfter() {

+		return pi.getEndOffset();

+	}

 }

diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/InsertTextEdit.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/InsertTextEdit.java
index 724eafb..946db9b 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/InsertTextEdit.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/InsertTextEdit.java
@@ -44,18 +44,25 @@
 	protected void performUndo() throws CannotUndoException {

 		try {

 			document.delete(new ContentRange(offset, offset + text.length() - 1));

-		} catch (final DocumentValidationException ex) {

-			throw new CannotUndoException();

+		} catch (final DocumentValidationException e) {

+			throw new CannotUndoException(e);

 		}

 	}

 

 	@Override

-	protected void performRedo() throws CannotRedoException {

+	protected void performRedo() throws CannotApplyException {

 		try {

 			document.insertText(offset, text);

-		} catch (final DocumentValidationException ex) {

-			throw new CannotRedoException();

+		} catch (final DocumentValidationException e) {

+			throw new CannotApplyException(e);

 		}

 	}

 

+	public int getOffsetBefore() {

+		return offset;

+	}

+

+	public int getOffsetAfter() {

+		return offset + text.length();

+	}

 }

diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/JoinElementsAtOffsetEdit.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/JoinElementsAtOffsetEdit.java
new file mode 100644
index 0000000..787fe09
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/JoinElementsAtOffsetEdit.java
@@ -0,0 +1,105 @@
+/*******************************************************************************
+ * 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 org.eclipse.vex.core.provisional.dom.ContentRange;
+import org.eclipse.vex.core.provisional.dom.DocumentValidationException;
+import org.eclipse.vex.core.provisional.dom.IDocument;
+import org.eclipse.vex.core.provisional.dom.IDocumentFragment;
+import org.eclipse.vex.core.provisional.dom.IElement;
+
+/**
+ * @author Florian Thienel
+ */
+public class JoinElementsAtOffsetEdit extends AbstractUndoableEdit {
+
+	private final IDocument document;
+	private final int offset;
+	private ContentRange rangeToRestore = null;
+	private IDocumentFragment fragmentToRestore = null;
+	private int offsetAfter;
+
+	public JoinElementsAtOffsetEdit(final IDocument document, final int offset) {
+		this.document = document;
+		this.offset = offset;
+		offsetAfter = offset;
+	}
+
+	@Override
+	protected void performRedo() throws CannotApplyException {
+		final IElement headElement;
+		final IElement tailElement;
+		if (isBetweenMatchingElements(document, offset - 1)) {
+			headElement = document.getElementForInsertionAt(offset - 2);
+			tailElement = document.getElementForInsertionAt(offset);
+		} else if (isBetweenMatchingElements(document, offset)) {
+			headElement = document.getElementForInsertionAt(offset - 1);
+			tailElement = document.getElementForInsertionAt(offset + 1);
+		} else if (isBetweenMatchingElements(document, offset + 1)) {
+			headElement = document.getElementForInsertionAt(offset);
+			tailElement = document.getElementForInsertionAt(offset + 2);
+		} else {
+			throw new CannotApplyException("The given offset " + offset + " is not between matching elements!");
+		}
+
+		final IDocumentFragment tailElementContent;
+		if (!tailElement.isEmpty()) {
+			tailElementContent = document.getFragment(tailElement.getRange().resizeBy(1, -1));
+		} else {
+			tailElementContent = null;
+		}
+
+		offsetAfter = headElement.getEndOffset();
+		fragmentToRestore = document.getFragment(headElement.getRange().union(tailElement.getRange()));
+
+		try {
+			document.delete(tailElement.getRange());
+			if (tailElementContent != null) {
+				document.insertFragment(headElement.getEndOffset(), tailElementContent);
+			}
+			rangeToRestore = headElement.getRange();
+		} catch (final DocumentValidationException e) {
+			throw new CannotApplyException(e);
+		}
+	}
+
+	public static boolean isBetweenMatchingElements(final IDocument document, final int offset) {
+		if (offset <= 1 || offset >= document.getLength() - 1) {
+			return false;
+		}
+		final IElement e1 = document.getElementForInsertionAt(offset - 1);
+		final IElement e2 = document.getElementForInsertionAt(offset + 1);
+		return e1 != e2 && e1 != null && e2 != null && e1.getParent() == e2.getParent() && e1.isKindOf(e2);
+	}
+
+	@Override
+	protected void performUndo() throws CannotUndoException {
+		try {
+			document.delete(rangeToRestore);
+			document.insertFragment(rangeToRestore.getStartOffset(), fragmentToRestore);
+			rangeToRestore = null;
+			fragmentToRestore = null;
+		} catch (final DocumentValidationException e) {
+			throw new CannotUndoException(e);
+		}
+	}
+
+	@Override
+	public int getOffsetBefore() {
+		return offset;
+	}
+
+	@Override
+	public int getOffsetAfter() {
+		return offsetAfter;
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/validator/WTPVEXValidator.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/validator/WTPVEXValidator.java
index 7617551..4c744b5 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/validator/WTPVEXValidator.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/validator/WTPVEXValidator.java
@@ -280,6 +280,9 @@
 				result.add(createQualifiedElementName(childDeclaration));
 			}
 		}
+		if (elementDeclaration.getContentType() == CMElementDeclaration.PCDATA || elementDeclaration.getContentType() == CMElementDeclaration.MIXED) {
+			result.add(PCDATA);
+		}
 		return result;
 	}
 
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/CssBasedBoxModelBuilder.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/CssBasedBoxModelBuilder.java
new file mode 100644
index 0000000..4082aba
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/CssBasedBoxModelBuilder.java
@@ -0,0 +1,597 @@
+/*******************************************************************************
+ * 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.visualization;
+
+import static org.eclipse.vex.core.internal.boxes.BoxFactory.inlineContainer;
+import static org.eclipse.vex.core.internal.boxes.BoxFactory.listItem;
+import static org.eclipse.vex.core.internal.boxes.BoxFactory.nodeReference;
+import static org.eclipse.vex.core.internal.boxes.BoxFactory.nodeReferenceWithInlineContent;
+import static org.eclipse.vex.core.internal.boxes.BoxFactory.nodeReferenceWithText;
+import static org.eclipse.vex.core.internal.boxes.BoxFactory.rootBox;
+import static org.eclipse.vex.core.internal.boxes.BoxFactory.verticalBlock;
+import static org.eclipse.vex.core.internal.visualization.CssBoxFactory.endOffsetPlaceholder;
+import static org.eclipse.vex.core.internal.visualization.CssBoxFactory.endTag;
+import static org.eclipse.vex.core.internal.visualization.CssBoxFactory.frame;
+import static org.eclipse.vex.core.internal.visualization.CssBoxFactory.graphicalBullet;
+import static org.eclipse.vex.core.internal.visualization.CssBoxFactory.image;
+import static org.eclipse.vex.core.internal.visualization.CssBoxFactory.list;
+import static org.eclipse.vex.core.internal.visualization.CssBoxFactory.nodeTag;
+import static org.eclipse.vex.core.internal.visualization.CssBoxFactory.paragraph;
+import static org.eclipse.vex.core.internal.visualization.CssBoxFactory.startTag;
+import static org.eclipse.vex.core.internal.visualization.CssBoxFactory.staticText;
+import static org.eclipse.vex.core.internal.visualization.CssBoxFactory.textContent;
+
+import java.net.MalformedURLException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+import org.eclipse.core.runtime.QualifiedName;
+import org.eclipse.vex.core.internal.boxes.IBulletFactory;
+import org.eclipse.vex.core.internal.boxes.IInlineBox;
+import org.eclipse.vex.core.internal.boxes.IParentBox;
+import org.eclipse.vex.core.internal.boxes.IStructuralBox;
+import org.eclipse.vex.core.internal.boxes.Image;
+import org.eclipse.vex.core.internal.boxes.InlineContainer;
+import org.eclipse.vex.core.internal.boxes.LineWrappingRule;
+import org.eclipse.vex.core.internal.boxes.Paragraph;
+import org.eclipse.vex.core.internal.boxes.RootBox;
+import org.eclipse.vex.core.internal.boxes.TextContent;
+import org.eclipse.vex.core.internal.css.AttributeDependendContent;
+import org.eclipse.vex.core.internal.css.BulletStyle;
+import org.eclipse.vex.core.internal.css.CSS;
+import org.eclipse.vex.core.internal.css.IPropertyContent;
+import org.eclipse.vex.core.internal.css.IPropertyContentVisitor;
+import org.eclipse.vex.core.internal.css.ImageContent;
+import org.eclipse.vex.core.internal.css.ProcessingInstructionTargetContent;
+import org.eclipse.vex.core.internal.css.StyleSheet;
+import org.eclipse.vex.core.internal.css.Styles;
+import org.eclipse.vex.core.internal.css.Styles.PseudoElement;
+import org.eclipse.vex.core.internal.css.TextualContent;
+import org.eclipse.vex.core.internal.css.URIContent;
+import org.eclipse.vex.core.internal.dom.CollectingNodeTraversal;
+import org.eclipse.vex.core.provisional.dom.BaseNodeVisitorWithResult;
+import org.eclipse.vex.core.provisional.dom.ContentRange;
+import org.eclipse.vex.core.provisional.dom.IComment;
+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;
+import org.eclipse.vex.core.provisional.dom.IProcessingInstruction;
+import org.eclipse.vex.core.provisional.dom.IText;
+import org.eclipse.vex.core.provisional.dom.IValidator;
+import org.eclipse.vex.core.provisional.dom.MultilineText;
+
+/**
+ * @author Florian Thienel
+ */
+public class CssBasedBoxModelBuilder implements IBoxModelBuilder {
+
+	private final StyleSheet styleSheet;
+
+	public CssBasedBoxModelBuilder(final StyleSheet styleSheet) {
+		this.styleSheet = styleSheet;
+	}
+
+	@Override
+	public RootBox visualizeRoot(final INode node) {
+		final IDocument document = node.getDocument();
+		return rootBox(asStructuralBox(visualize(document)));
+	}
+
+	@Override
+	public IStructuralBox visualizeStructure(final INode node) {
+		return asStructuralBox(visualize(node));
+	}
+
+	private IStructuralBox asStructuralBox(final VisualizeResult visualizeResult) {
+		if (visualizeResult.inline) {
+			return visualizeAsBlock(visualizeResult.node, visualizeResult.styles, visualizeResult.childrenResults);
+		} else {
+			return visualizeResult.structuralBox;
+		}
+	}
+
+	@Override
+	public IInlineBox visualizeInline(final INode node) {
+		return asInlineBox(visualize(node));
+	}
+
+	private IInlineBox asInlineBox(final VisualizeResult visualizeResult) {
+		if (visualizeResult.inline) {
+			return visualizeResult.inlineBox;
+		} else {
+			return visualizeInline(visualizeResult.node, visualizeResult.styles, visualizeResult.childrenResults);
+		}
+	}
+
+	/*
+	 * Traverse, coarse decision depending on "display" property, collect
+	 */
+
+	private VisualizeResult visualize(final INode node) {
+		return node.accept(new CollectingNodeTraversal<VisualizeResult>() {
+			@Override
+			public VisualizeResult visit(final IDocument document) {
+				final Styles styles = styleSheet.getStyles(document);
+				final Collection<VisualizeResult> childrenResults = traverseChildren(document);
+				return new VisualizeResult(document, styles, childrenResults, nodeReference(document, visualizeAsBlock(document, styles, childrenResults)));
+			}
+
+			@Override
+			public VisualizeResult visit(final IDocumentFragment documentFragment) {
+				final Styles styles = styleSheet.getStyles(documentFragment);
+				final Collection<VisualizeResult> childrenResults = traverseChildren(documentFragment);
+				return new VisualizeResult(documentFragment, styles, childrenResults, nodeReference(documentFragment, visualizeAsBlock(documentFragment, styles, childrenResults)));
+			}
+
+			@Override
+			public VisualizeResult visit(final IElement element) {
+				final Styles styles = styleSheet.getStyles(element);
+				final Collection<VisualizeResult> childrenResults = traverseChildren(element);
+				if (isListRoot(styles)) {
+					return new VisualizeResult(element, styles, childrenResults, visualizeAsList(element, styles, childrenResults));
+				} else if (isListItem(styles)) {
+					return new VisualizeResult(element, styles, childrenResults, visualizeAsListItem(element, styles, childrenResults));
+				} else if (isDisplayedAsBlock(styles)) {
+					return new VisualizeResult(element, styles, childrenResults, visualizeAsBlock(element, styles, childrenResults));
+				} else {
+					return new VisualizeResult(element, styles, childrenResults, visualizeInline(element, styles, childrenResults));
+				}
+			}
+
+			@Override
+			public VisualizeResult visit(final IComment comment) {
+				final Styles styles = styleSheet.getStyles(comment);
+				final List<VisualizeResult> childrenResults = Collections.<VisualizeResult> emptyList();
+				if (isDisplayedAsBlock(styles)) {
+					return new VisualizeResult(comment, styles, childrenResults, visualizeAsBlock(comment, styles, childrenResults));
+				} else {
+					return new VisualizeResult(comment, styles, childrenResults, visualizeInline(comment, styles, childrenResults));
+				}
+			}
+
+			@Override
+			public VisualizeResult visit(final IProcessingInstruction pi) {
+				final Styles styles = styleSheet.getStyles(pi);
+				final List<VisualizeResult> childrenResults = Collections.<VisualizeResult> emptyList();
+				if (isDisplayedAsBlock(styles)) {
+					return new VisualizeResult(pi, styles, childrenResults, visualizeAsBlock(pi, styles, childrenResults));
+				} else {
+					return new VisualizeResult(pi, styles, childrenResults, visualizeInline(pi, styles, childrenResults));
+				}
+			}
+
+			@Override
+			public VisualizeResult visit(final IText text) {
+				final Styles styles = styleSheet.getStyles(text);
+				final List<VisualizeResult> childrenResults = Collections.<VisualizeResult> emptyList();
+				return new VisualizeResult(text, styles, childrenResults, visualizeInline(text, styles, childrenResults));
+			}
+		});
+	}
+
+	private static boolean isListRoot(final Styles styles) {
+		final String listStyleType = styles.getListStyleType();
+		return listStyleType != null && !CSS.NONE.equals(listStyleType);
+	}
+
+	private static boolean isListItem(final Styles styles) {
+		return CSS.LIST_ITEM.equals(styles.getDisplay());
+	}
+
+	private static boolean isDisplayedAsBlock(final Styles styles) {
+		// currently we can only render blocks or inline, hence everything that is not inline must be a block
+		return !isDisplayedInline(styles);
+	}
+
+	private static boolean isDisplayedInline(final Styles styles) {
+		return CSS.INLINE.equals(styles.getDisplay());
+	}
+
+	private static boolean isPreservingWhitespace(final Styles styles) {
+		return CSS.PRE.equals(styles.getWhiteSpace());
+	}
+
+	private static boolean isWrappedInInlineMarkers(final Styles styles) {
+		return CSS.NORMAL.equals(styles.getInlineMarker());
+	}
+
+	/*
+	 * Render as List
+	 */
+
+	private IStructuralBox visualizeAsList(final IElement element, final Styles styles, final Collection<VisualizeResult> childrenResults) {
+		return list(visualizeAsBlock(element, styles, childrenResults), styles, visualizeBullet(styles));
+	}
+
+	private static IBulletFactory visualizeBullet(final Styles styles) {
+		return new IBulletFactory() {
+			@Override
+			public IInlineBox createBullet(final BulletStyle bulletStyle, final int itemIndex, final int itemCount) {
+				final IInlineBox bullet;
+				if (bulletStyle.type.isTextual()) {
+					bullet = staticText(bulletStyle.getBulletAsText(itemIndex, itemCount), styles);
+				} else {
+					bullet = graphicalBullet(bulletStyle.type, styles);
+				}
+				return bullet;
+			}
+		};
+	}
+
+	/*
+	 * Render as ListItem
+	 */
+
+	private IStructuralBox visualizeAsListItem(final IElement element, final Styles styles, final Collection<VisualizeResult> childrenResults) {
+		final IStructuralBox content = visualizeStructuralElementContent(element, styles, childrenResults);
+		return wrapUpStructuralElementContent(element, styles, childrenResults, listItem(content));
+	}
+
+	/*
+	 * Render as Block
+	 */
+
+	private IStructuralBox visualizeAsBlock(final INode node, final Styles styles, final Collection<VisualizeResult> childrenResults) {
+		return node.accept(new BaseNodeVisitorWithResult<IStructuralBox>() {
+			@Override
+			public IStructuralBox visit(final IDocument document) {
+				return visualizeChildrenAsStructure(document, styles, childrenResults, verticalBlock());
+			}
+
+			@Override
+			public IStructuralBox visit(final IDocumentFragment fragment) {
+				return visualizeChildrenAsStructure(fragment, styles, childrenResults, verticalBlock());
+			}
+
+			@Override
+			public IStructuralBox visit(final IElement element) {
+				final IStructuralBox content = visualizeStructuralElementContent(element, styles, childrenResults);
+				return wrapUpStructuralElementContent(element, styles, childrenResults, content);
+			}
+
+			@Override
+			public IStructuralBox visit(final IComment comment) {
+				final Paragraph inlineElementContent;
+				if (comment.isEmpty()) {
+					inlineElementContent = placeholderForEmptyNode(comment, styles, paragraph(styles));
+				} else {
+					inlineElementContent = paragraph(styles, visualizeText(comment.getContent(), comment.getRange().resizeBy(1, -1), comment, styles));
+				}
+
+				return nodeReferenceWithText(comment, surroundWithPseudoElements(frame(surroundWithInlinePseudoElements(inlineElementContent, comment, styles), styles), comment, styles));
+			}
+
+			@Override
+			public IStructuralBox visit(final IProcessingInstruction pi) {
+				final Paragraph inlineElementContent;
+				if (pi.isEmpty()) {
+					inlineElementContent = placeholderForEmptyNode(pi, styles, paragraph(styles));
+				} else {
+					inlineElementContent = paragraph(styles, visualizeText(pi.getContent(), pi.getRange().resizeBy(1, -1), pi, styles));
+				}
+
+				return nodeReferenceWithText(pi, surroundWithPseudoElements(frame(surroundWithInlinePseudoElements(inlineElementContent, pi, styles), styles), pi, styles));
+			}
+		});
+	}
+
+	private IStructuralBox visualizeStructuralElementContent(final IElement element, final Styles styles, final Collection<VisualizeResult> childrenResults) {
+		if (isElementWithNoContentAllowed(element)) {
+			return visualizeStructuralElementWithNoContentAllowed(styles, element);
+		} else if (element.isEmpty()) {
+			return placeholderForEmptyNode(element, styles, paragraph(styles));
+		} else {
+			return visualizeChildrenAsStructure(element, styles, childrenResults, verticalBlock());
+		}
+	}
+
+	private static IStructuralBox visualizeStructuralElementWithNoContentAllowed(final Styles styles, final IElement element) {
+		return paragraph(styles, visualizeInlineElementWithNoContentAllowed(element, styles));
+	}
+
+	private static IInlineBox visualizeInlineElementWithNoContentAllowed(final INode node, final Styles styles) {
+		if (!styles.isContentDefined()) {
+			return nodeTag(node, styles);
+		}
+		return visualizeContentProperty(node, styles, inlineContainer());
+	}
+
+	private <P extends IParentBox<IStructuralBox>> P visualizeChildrenAsStructure(final INode node, final Styles styles, final Iterable<VisualizeResult> childrenResults, final P parentBox) {
+		final LinkedList<VisualizeResult> pendingInline = new LinkedList<VisualizeResult>();
+		for (final VisualizeResult visualizeResult : childrenResults) {
+			if (visualizeResult.inline) {
+				pendingInline.add(visualizeResult);
+			} else {
+				if (!pendingInline.isEmpty()) {
+					parentBox.appendChild(visualizeInlineNodeContent(node, styles, pendingInline, paragraph(styles)));
+				}
+				pendingInline.clear();
+				parentBox.appendChild(asStructuralBox(visualizeResult));
+			}
+		}
+		if (!pendingInline.isEmpty()) {
+			parentBox.appendChild(visualizeInlineNodeContent(node, styles, pendingInline, paragraph(styles)));
+		}
+		return parentBox;
+	}
+
+	private static IStructuralBox wrapUpStructuralElementContent(final IElement element, final Styles styles, final Collection<VisualizeResult> childrenResults, final IStructuralBox content) {
+		final boolean mayContainText = mayContainText(element);
+		final boolean containsInlineContent = containsInlineContent(childrenResults);
+		if (mayContainText) {
+			return nodeReferenceWithText(element, surroundWithPseudoElements(frame(content, styles), element, styles));
+		} else if (containsInlineContent) {
+			return nodeReferenceWithInlineContent(element, surroundWithPseudoElements(frame(content, styles), element, styles));
+		} else {
+			return nodeReference(element, surroundWithPseudoElements(frame(content, styles), element, styles));
+		}
+	}
+
+	private static IStructuralBox surroundWithPseudoElements(final IStructuralBox content, final INode node, final Styles styles) {
+		final IStructuralBox pseudoElementBefore = visualizePseudoElementAsBlock(styles, node, PseudoElement.BEFORE);
+		final IStructuralBox pseudoElementAfter = visualizePseudoElementAsBlock(styles, node, PseudoElement.AFTER);
+
+		if (pseudoElementBefore == null && pseudoElementAfter == null) {
+			return content;
+		}
+
+		return verticalBlock(pseudoElementBefore, content, pseudoElementAfter);
+	}
+
+	private static IStructuralBox visualizePseudoElementAsBlock(final Styles styles, final INode node, final PseudoElement pseudoElement) {
+		if (!styles.hasPseudoElement(pseudoElement)) {
+			return null;
+		}
+
+		final Styles pseudoElementStyles = styles.getPseudoElementStyles(pseudoElement);
+		if (!isDisplayedAsBlock(pseudoElementStyles)) {
+			return null;
+		}
+
+		return frame(visualizeContentProperty(node, pseudoElementStyles, paragraph(pseudoElementStyles)), pseudoElementStyles);
+	}
+
+	private static IInlineBox visualizePseudoElementInline(final Styles styles, final INode node, final PseudoElement pseudoElement) {
+		if (!styles.hasPseudoElement(pseudoElement)) {
+			return null;
+		}
+
+		final Styles pseudoElementStyles = styles.getPseudoElementStyles(pseudoElement);
+		if (!isDisplayedInline(pseudoElementStyles)) {
+			return null;
+		}
+
+		return frame(visualizeContentProperty(node, pseudoElementStyles, inlineContainer()), pseudoElementStyles);
+	}
+
+	private static <P extends IParentBox<IInlineBox>> P visualizeContentProperty(final INode node, final Styles styles, final P parent) {
+		for (final IPropertyContent part : styles.getContent()) {
+			final IInlineBox box = part.accept(new IPropertyContentVisitor<IInlineBox>() {
+				@Override
+				public IInlineBox visit(final TextualContent content) {
+					return staticText(content.toString(), styles);
+				}
+
+				@Override
+				public IInlineBox visit(final AttributeDependendContent content) {
+					return staticText(content.toString(), styles);
+				}
+
+				@Override
+				public IInlineBox visit(final ProcessingInstructionTargetContent content) {
+					return staticText(content.toString(), styles);
+				}
+
+				@Override
+				public IInlineBox visit(final URIContent content) {
+					final String imageUri = content.uri.toString();
+					final Image image = new Image();
+					image.setImageUrl(styles.resolveUrl(imageUri));
+					return image;
+				}
+
+				@Override
+				public IInlineBox visit(final ImageContent content) {
+					try {
+						return image(content.getResolvedImageURL(), styles);
+					} catch (final MalformedURLException e) {
+						// TODO log error, render error information
+						e.printStackTrace();
+						return staticText(e.getMessage(), styles);
+					}
+				}
+			});
+			if (box != null) {
+				parent.appendChild(box);
+			}
+		}
+		return parent;
+	}
+
+	private static boolean containsInlineContent(final Collection<VisualizeResult> visualizeResults) {
+		for (final VisualizeResult visualizeResult : visualizeResults) {
+			if (visualizeResult.inline) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	private static boolean mayContainText(final IElement element) {
+		final Set<QualifiedName> validItems = element.getDocument().getValidator().getValidItems(element);
+		return validItems.contains(IValidator.PCDATA);
+	}
+
+	private static boolean isElementWithNoContentAllowed(final IElement element) {
+		final Set<QualifiedName> validItems = element.getDocument().getValidator().getValidItems(element);
+		return validItems.isEmpty() && !element.hasChildren();
+	}
+
+	/*
+	 * Render inline elements
+	 */
+
+	private IInlineBox visualizeInline(final INode node, final Styles styles, final Collection<VisualizeResult> childrenResults) {
+		return node.accept(new BaseNodeVisitorWithResult<IInlineBox>() {
+			@Override
+			public IInlineBox visit(final IElement element) {
+				if (isElementWithNoContentAllowed(element)) {
+					return nodeReference(element,
+							frame(surroundWithInlinePseudoElements(inlineContainer(visualizeInlineElementWithNoContentAllowed(element, styles)), element, styles), styles));
+				}
+
+				final InlineContainer inlineElementContent = surroundWithInlineMarkers(element, styles,
+						visualizeInlineNodeContent(element, styles, childrenResults,
+								inlineContainer()));
+
+				if (mayContainText(element)) {
+					return nodeReferenceWithText(element, frame(inlineElementContent, styles));
+				} else {
+					return nodeReference(element, frame(inlineElementContent, styles));
+				}
+			}
+
+			@Override
+			public IInlineBox visit(final IComment comment) {
+				final InlineContainer inlineElementContent;
+				if (comment.isEmpty()) {
+					inlineElementContent = placeholderForEmptyNode(comment, styles, inlineContainer());
+				} else {
+					inlineElementContent = inlineContainer(visualizeText(comment.getContent(), comment.getRange().resizeBy(1, -1), comment, styles));
+				}
+
+				return nodeReferenceWithText(comment, frame(surroundWithInlinePseudoElements(inlineElementContent, comment, styles), styles));
+			}
+
+			@Override
+			public IInlineBox visit(final IProcessingInstruction pi) {
+				final InlineContainer inlineElementContent;
+				if (pi.isEmpty()) {
+					inlineElementContent = placeholderForEmptyNode(pi, styles, inlineContainer());
+				} else {
+					inlineElementContent = inlineContainer(visualizeText(pi.getContent(), pi.getRange().resizeBy(1, -1), pi, styles));
+				}
+
+				return nodeReferenceWithText(pi, frame(surroundWithInlinePseudoElements(inlineElementContent, pi, styles), styles));
+			}
+
+			@Override
+			public IInlineBox visit(final IText text) {
+				return visualizeText(text.getContent(), text.getRange(), text.getParent(), styles);
+			}
+
+		});
+	}
+
+	private static <P extends IParentBox<IInlineBox>> P surroundWithInlineMarkers(final INode node, final Styles styles, final P parent) {
+		if (isWrappedInInlineMarkers(styles)) {
+			parent.prependChild(startTag(node, styles));
+			parent.appendChild(endTag(node, styles));
+		}
+		return parent;
+	}
+
+	private <P extends IParentBox<IInlineBox>> P visualizeInlineNodeContent(final INode node, final Styles styles, final Collection<VisualizeResult> childrenResults, final P parent) {
+		if (!childrenResults.isEmpty()) {
+			return surroundWithInlinePseudoElements(visualizeChildrenInline(childrenResults, visualizeContentProperty(node, styles, parent)), node, styles);
+		} else {
+			return surroundWithInlinePseudoElements(placeholderForEmptyNode(node, styles, visualizeContentProperty(node, styles, parent)), node, styles);
+		}
+	}
+
+	private <P extends IParentBox<IInlineBox>> P visualizeChildrenInline(final Iterable<VisualizeResult> childrenResults, final P parentBox) {
+		for (final VisualizeResult visualizeResult : childrenResults) {
+			parentBox.appendChild(asInlineBox(visualizeResult));
+		}
+		return parentBox;
+	}
+
+	private static IInlineBox visualizeText(final IContent content, final ContentRange textRange, final INode parentNode, final Styles styles) {
+		if (isPreservingWhitespace(styles)) {
+			return visualizeAsMultilineText(content, textRange, parentNode, styles);
+		} else {
+			return textContent(content, textRange, styles);
+		}
+	}
+
+	private static IInlineBox visualizeAsMultilineText(final IContent content, final ContentRange textRange, final INode parentNode, final Styles styles) {
+		final InlineContainer lineContainer = inlineContainer();
+		final MultilineText lines = content.getMultilineText(textRange);
+		for (int i = 0; i < lines.size(); i += 1) {
+			final TextContent textLine = textContent(content, lines.getRange(i), styles);
+			if (content.isLineBreak(lines.getRange(i).getEndOffset())) {
+				textLine.setLineWrappingAtEnd(LineWrappingRule.REQUIRED);
+			}
+			lineContainer.appendChild(textLine);
+		}
+
+		if (textRange.getEndOffset() == parentNode.getEndOffset() - 1 && content.isLineBreak(textRange.getEndOffset())) {
+			lineContainer.appendChild(endOffsetPlaceholder(parentNode, styles));
+		}
+
+		return lineContainer;
+	}
+
+	private static <P extends IParentBox<IInlineBox>> P surroundWithInlinePseudoElements(final P parent, final INode node, final Styles styles) {
+		final IInlineBox pseudoElementBefore = visualizePseudoElementInline(styles, node, PseudoElement.BEFORE);
+		final IInlineBox pseudoElementAfter = visualizePseudoElementInline(styles, node, PseudoElement.AFTER);
+
+		if (pseudoElementBefore != null) {
+			parent.prependChild(pseudoElementBefore);
+		}
+		if (pseudoElementAfter != null) {
+			parent.appendChild(pseudoElementAfter);
+		}
+
+		return parent;
+	}
+
+	private static <P extends IParentBox<IInlineBox>> P placeholderForEmptyNode(final INode node, final Styles styles, final P parent) {
+		parent.appendChild(endOffsetPlaceholder(node, styles));
+		//		if (false) { // TODO allow to provide a placeholder text in the CSS
+		//			parent.appendChild(staticText(MessageFormat.format("[placeholder for empty <{0}> element]", element.getLocalName()), styles));
+		//		}
+		return parent;
+	}
+
+	private static class VisualizeResult {
+		public final INode node;
+		public final Styles styles;
+		public final Collection<VisualizeResult> childrenResults;
+		public final boolean inline;
+		public final IInlineBox inlineBox;
+		public final IStructuralBox structuralBox;
+
+		public VisualizeResult(final INode node, final Styles styles, final Collection<VisualizeResult> childrenResults, final IStructuralBox box) {
+			this.node = node;
+			this.styles = styles;
+			this.childrenResults = childrenResults;
+			inline = false;
+			inlineBox = null;
+			structuralBox = box;
+		}
+
+		public VisualizeResult(final INode node, final Styles styles, final Collection<VisualizeResult> childrenResults, final IInlineBox box) {
+			this.node = node;
+			this.styles = styles;
+			this.childrenResults = childrenResults;
+			inline = true;
+			inlineBox = box;
+			structuralBox = null;
+		}
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/CssBoxFactory.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/CssBoxFactory.java
new file mode 100644
index 0000000..bb618fd
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/CssBoxFactory.java
@@ -0,0 +1,208 @@
+/*******************************************************************************
+ * 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.visualization;
+
+import java.net.URL;
+
+import org.eclipse.vex.core.internal.boxes.Border;
+import org.eclipse.vex.core.internal.boxes.BorderLine;
+import org.eclipse.vex.core.internal.boxes.GraphicalBullet;
+import org.eclipse.vex.core.internal.boxes.IBulletFactory;
+import org.eclipse.vex.core.internal.boxes.IInlineBox;
+import org.eclipse.vex.core.internal.boxes.IStructuralBox;
+import org.eclipse.vex.core.internal.boxes.Image;
+import org.eclipse.vex.core.internal.boxes.InlineFrame;
+import org.eclipse.vex.core.internal.boxes.LineWrappingRule;
+import org.eclipse.vex.core.internal.boxes.List;
+import org.eclipse.vex.core.internal.boxes.Margin;
+import org.eclipse.vex.core.internal.boxes.NodeEndOffsetPlaceholder;
+import org.eclipse.vex.core.internal.boxes.NodeTag;
+import org.eclipse.vex.core.internal.boxes.Padding;
+import org.eclipse.vex.core.internal.boxes.Paragraph;
+import org.eclipse.vex.core.internal.boxes.Square;
+import org.eclipse.vex.core.internal.boxes.StaticText;
+import org.eclipse.vex.core.internal.boxes.StructuralFrame;
+import org.eclipse.vex.core.internal.boxes.TextContent;
+import org.eclipse.vex.core.internal.core.FontSpec;
+import org.eclipse.vex.core.internal.core.LineStyle;
+import org.eclipse.vex.core.internal.core.TextAlign;
+import org.eclipse.vex.core.internal.css.BulletStyle;
+import org.eclipse.vex.core.internal.css.CSS;
+import org.eclipse.vex.core.internal.css.Styles;
+import org.eclipse.vex.core.provisional.dom.ContentRange;
+import org.eclipse.vex.core.provisional.dom.IContent;
+import org.eclipse.vex.core.provisional.dom.INode;
+
+public class CssBoxFactory {
+
+	public static StructuralFrame frame(final IStructuralBox component, final Styles styles) {
+		final StructuralFrame frame = new StructuralFrame();
+		frame.setComponent(component);
+		frame.setMargin(margin(styles));
+		frame.setBorder(border(styles));
+		frame.setPadding(padding(styles));
+		frame.setBackgroundColor(styles.getBackgroundColor());
+		return frame;
+	}
+
+	public static InlineFrame frame(final IInlineBox component, final Styles styles) {
+		final InlineFrame frame = new InlineFrame();
+		frame.setComponent(component);
+		frame.setMargin(margin(styles));
+		frame.setBorder(border(styles));
+		frame.setPadding(padding(styles));
+		frame.setBackgroundColor(styles.getBackgroundColor());
+		return frame;
+	}
+
+	public static List list(final IStructuralBox component, final Styles styles, final IBulletFactory bulletFactory) {
+		final List list = new List();
+		list.setBulletStyle(BulletStyle.fromStyles(styles));
+		list.setBulletFactory(bulletFactory);
+		list.setComponent(component);
+		return list;
+	}
+
+	public static Paragraph paragraph(final Styles styles, final IInlineBox... children) {
+		final Paragraph paragraph = new Paragraph();
+		for (final IInlineBox child : children) {
+			paragraph.appendChild(child);
+		}
+		paragraph.setTextAlign(textAlign(styles));
+		return paragraph;
+	}
+
+	public static TextContent textContent(final IContent content, final ContentRange range, final Styles styles) {
+		final TextContent textContent = new TextContent();
+		textContent.setContent(content, range);
+		textContent.setFont(font(styles));
+		textContent.setColor(styles.getColor());
+		return textContent;
+	}
+
+	public static TextContent textContentWithLineBreak(final IContent content, final ContentRange range, final Styles styles) {
+		final TextContent textContent = textContent(content, range, styles);
+		textContent.setLineWrappingAtEnd(LineWrappingRule.REQUIRED);
+		return textContent;
+	}
+
+	public static NodeEndOffsetPlaceholder endOffsetPlaceholder(final INode node, final Styles styles) {
+		final NodeEndOffsetPlaceholder contentPlaceholder = new NodeEndOffsetPlaceholder();
+		contentPlaceholder.setNode(node);
+		contentPlaceholder.setFont(font(styles));
+		return contentPlaceholder;
+	}
+
+	public static StaticText staticText(final String text, final Styles styles) {
+		final StaticText staticText = new StaticText();
+		staticText.setText(text);
+		staticText.setFont(font(styles));
+		staticText.setColor(styles.getColor());
+		return staticText;
+	}
+
+	public static StaticText staticTextWithLineBreak(final String text, final Styles styles) {
+		final StaticText staticText = staticText(text, styles);
+		staticText.setLineWrappingAtEnd(LineWrappingRule.REQUIRED);
+		return staticText;
+	}
+
+	public static Image image(final URL imageUrl, final Styles styles) {
+		final Image image = new Image();
+		image.setImageUrl(imageUrl);
+		image.setPreferredWidth(styles.getElementWidth());
+		image.setPreferredHeight(styles.getElementHeight());
+		return image;
+	}
+
+	public static Square square(final int size, final Styles styles) {
+		final Square square = new Square();
+		square.setSize(size);
+		square.setColor(styles.getColor());
+		return square;
+	}
+
+	public static GraphicalBullet graphicalBullet(final BulletStyle.Type type, final Styles styles) {
+		final GraphicalBullet bullet = new GraphicalBullet();
+		bullet.setType(type);
+		bullet.setFont(font(styles));
+		bullet.setColor(styles.getColor());
+		return bullet;
+	}
+
+	public static NodeTag nodeTag(final INode node, final Styles styles) {
+		final NodeTag nodeTag = new NodeTag();
+		nodeTag.setKind(NodeTag.Kind.NODE);
+		nodeTag.setNode(node);
+		nodeTag.setColor(styles.getColor());
+		nodeTag.setShowText(true);
+		return nodeTag;
+	}
+
+	public static NodeTag startTag(final INode node, final Styles styles) {
+		final NodeTag nodeTag = new NodeTag();
+		nodeTag.setKind(NodeTag.Kind.START);
+		nodeTag.setNode(node);
+		nodeTag.setColor(styles.getColor());
+		nodeTag.setShowText(false);
+		return nodeTag;
+	}
+
+	public static NodeTag endTag(final INode node, final Styles styles) {
+		final NodeTag nodeTag = new NodeTag();
+		nodeTag.setKind(NodeTag.Kind.END);
+		nodeTag.setNode(node);
+		nodeTag.setColor(styles.getColor());
+		nodeTag.setShowText(false);
+		return nodeTag;
+	}
+
+	public static Margin margin(final Styles styles) {
+		return new Margin(styles.getMarginTop(), styles.getMarginLeft(), styles.getMarginBottom(), styles.getMarginRight());
+	}
+
+	public static Border border(final Styles styles) {
+		final BorderLine top = new BorderLine(styles.getBorderTopWidth(), borderStyle(styles.getBorderTopStyle()), styles.getBorderTopColor());
+		final BorderLine left = new BorderLine(styles.getBorderLeftWidth(), borderStyle(styles.getBorderLeftStyle()), styles.getBorderLeftColor());
+		final BorderLine bottom = new BorderLine(styles.getBorderBottomWidth(), borderStyle(styles.getBorderBottomStyle()), styles.getBorderBottomColor());
+		final BorderLine right = new BorderLine(styles.getBorderRightWidth(), borderStyle(styles.getBorderRightStyle()), styles.getBorderRightColor());
+		return new Border(top, left, bottom, right);
+	}
+
+	public static LineStyle borderStyle(final String borderStyleName) {
+		if (CSS.DOTTED.equals(borderStyleName)) {
+			return LineStyle.DOTTED;
+		} else if (CSS.DASHED.equals(borderStyleName)) {
+			return LineStyle.DASHED;
+		} else {
+			return LineStyle.SOLID;
+		}
+	}
+
+	public static Padding padding(final Styles styles) {
+		return new Padding(styles.getPaddingTop(), styles.getPaddingLeft(), styles.getPaddingBottom(), styles.getPaddingRight());
+	}
+
+	public static FontSpec font(final Styles styles) {
+		return styles.getFont();
+	}
+
+	public static TextAlign textAlign(final Styles styles) {
+		final String textAlign = styles.getTextAlign();
+		if (CSS.CENTER.equals(textAlign)) {
+			return TextAlign.CENTER;
+		} else if (CSS.RIGHT.equals(textAlign)) {
+			return TextAlign.RIGHT;
+		} else {
+			return TextAlign.LEFT;
+		}
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/DocumentRootVisualization.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/DocumentRootVisualization.java
new file mode 100644
index 0000000..dc0e292
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/DocumentRootVisualization.java
@@ -0,0 +1,25 @@
+/*******************************************************************************
+ * 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.visualization;
+
+import static org.eclipse.vex.core.internal.boxes.BoxFactory.nodeReference;
+import static org.eclipse.vex.core.internal.boxes.BoxFactory.rootBox;
+import static org.eclipse.vex.core.internal.boxes.BoxFactory.verticalBlock;
+
+import org.eclipse.vex.core.internal.boxes.RootBox;
+import org.eclipse.vex.core.provisional.dom.IDocument;
+
+public final class DocumentRootVisualization extends NodeVisualization<RootBox> {
+	@Override
+	public RootBox visit(final IDocument document) {
+		return rootBox(nodeReference(document, visualizeChildrenStructure(document.children(), verticalBlock())));
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/IBoxModelBuilder.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/IBoxModelBuilder.java
new file mode 100644
index 0000000..2074b69
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/IBoxModelBuilder.java
@@ -0,0 +1,29 @@
+/*******************************************************************************
+ * 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.visualization;
+
+import org.eclipse.vex.core.internal.boxes.IInlineBox;
+import org.eclipse.vex.core.internal.boxes.IStructuralBox;
+import org.eclipse.vex.core.internal.boxes.RootBox;
+import org.eclipse.vex.core.provisional.dom.INode;
+
+/**
+ * @author Florian Thienel
+ */
+public interface IBoxModelBuilder {
+
+	RootBox visualizeRoot(INode node);
+
+	IStructuralBox visualizeStructure(INode node);
+
+	IInlineBox visualizeInline(INode node);
+
+}
\ No newline at end of file
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/InlineElementVisualization.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/InlineElementVisualization.java
new file mode 100644
index 0000000..844b6d1
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/InlineElementVisualization.java
@@ -0,0 +1,52 @@
+/*******************************************************************************
+ * 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.visualization;
+
+import static org.eclipse.vex.core.internal.boxes.BoxFactory.frame;
+import static org.eclipse.vex.core.internal.boxes.BoxFactory.inlineContainer;
+import static org.eclipse.vex.core.internal.boxes.BoxFactory.nodeReferenceWithText;
+import static org.eclipse.vex.core.internal.boxes.BoxFactory.staticText;
+
+import org.eclipse.vex.core.internal.boxes.Border;
+import org.eclipse.vex.core.internal.boxes.IInlineBox;
+import org.eclipse.vex.core.internal.boxes.InlineContainer;
+import org.eclipse.vex.core.internal.boxes.Margin;
+import org.eclipse.vex.core.internal.boxes.Padding;
+import org.eclipse.vex.core.internal.core.Color;
+import org.eclipse.vex.core.internal.core.FontSpec;
+import org.eclipse.vex.core.provisional.dom.IElement;
+
+public class InlineElementVisualization extends NodeVisualization<IInlineBox> {
+	private static final FontSpec TIMES_NEW_ROMAN = new FontSpec("Times New Roman", FontSpec.PLAIN, 20.0f);
+
+	public InlineElementVisualization() {
+		super(1);
+	}
+
+	@Override
+	public IInlineBox visit(final IElement element) {
+		if (!"b".equals(element.getLocalName())) {
+			return super.visit(element);
+		}
+
+		return nodeReferenceWithText(element, frame(visualizeInlineElement(element), new Margin(4), new Border(2), new Padding(5), null));
+	}
+
+	private InlineContainer visualizeInlineElement(final IElement element) {
+		final InlineContainer container = inlineContainer();
+		if (element.hasChildren()) {
+			visualizeChildrenInline(element.children(), container);
+		} else {
+			container.appendChild(staticText(" ", TIMES_NEW_ROMAN, Color.BLACK));
+		}
+		return container;
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/NodeVisualization.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/NodeVisualization.java
new file mode 100644
index 0000000..485e02e
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/NodeVisualization.java
@@ -0,0 +1,64 @@
+/*******************************************************************************
+ * 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.visualization;
+
+import org.eclipse.vex.core.internal.boxes.IBox;
+import org.eclipse.vex.core.internal.boxes.IInlineBox;
+import org.eclipse.vex.core.internal.boxes.IParentBox;
+import org.eclipse.vex.core.internal.boxes.IStructuralBox;
+import org.eclipse.vex.core.provisional.dom.BaseNodeVisitorWithResult;
+import org.eclipse.vex.core.provisional.dom.INode;
+
+public class NodeVisualization<T extends IBox> extends BaseNodeVisitorWithResult<T> implements Comparable<NodeVisualization<?>> {
+	private final int priority;
+	private IBoxModelBuilder boxModelBuilder;
+
+	public NodeVisualization() {
+		this(0);
+	}
+
+	public NodeVisualization(final int priority) {
+		this.priority = priority;
+	}
+
+	public final T visualize(final INode node) {
+		return node.accept(this);
+	}
+
+	@Override
+	public final int compareTo(final NodeVisualization<?> other) {
+		return other.priority - priority;
+	}
+
+	public final void setChain(final IBoxModelBuilder boxModelBuilder) {
+		this.boxModelBuilder = boxModelBuilder;
+	}
+
+	protected final <P extends IParentBox<IStructuralBox>> P visualizeChildrenStructure(final Iterable<INode> children, final P parentBox) {
+		for (final INode child : children) {
+			final IStructuralBox childBox = boxModelBuilder.visualizeStructure(child);
+			if (childBox != null) {
+				parentBox.appendChild(childBox);
+			}
+		}
+		return parentBox;
+	}
+
+	protected final <P extends IParentBox<IInlineBox>> P visualizeChildrenInline(final Iterable<INode> children, final P parentBox) {
+		for (final INode child : children) {
+			final IInlineBox childBox = boxModelBuilder.visualizeInline(child);
+			if (childBox != null) {
+				parentBox.appendChild(childBox);
+			}
+		}
+		return parentBox;
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/ParagraphVisualization.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/ParagraphVisualization.java
new file mode 100644
index 0000000..e55c497
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/ParagraphVisualization.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.visualization;
+
+import static org.eclipse.vex.core.internal.boxes.BoxFactory.frame;
+import static org.eclipse.vex.core.internal.boxes.BoxFactory.nodeReferenceWithText;
+import static org.eclipse.vex.core.internal.boxes.BoxFactory.paragraph;
+import static org.eclipse.vex.core.internal.boxes.BoxFactory.staticText;
+
+import org.eclipse.vex.core.internal.boxes.Border;
+import org.eclipse.vex.core.internal.boxes.IStructuralBox;
+import org.eclipse.vex.core.internal.boxes.Margin;
+import org.eclipse.vex.core.internal.boxes.Padding;
+import org.eclipse.vex.core.internal.boxes.Paragraph;
+import org.eclipse.vex.core.internal.core.Color;
+import org.eclipse.vex.core.internal.core.FontSpec;
+import org.eclipse.vex.core.provisional.dom.IElement;
+
+public final class ParagraphVisualization extends NodeVisualization<IStructuralBox> {
+	private static final FontSpec TIMES_NEW_ROMAN = new FontSpec("Times New Roman", FontSpec.PLAIN, 20.0f);
+
+	public ParagraphVisualization() {
+		super(1);
+	}
+
+	@Override
+	public IStructuralBox visit(final IElement element) {
+		if (!"para".equals(element.getLocalName())) {
+			return super.visit(element);
+		}
+
+		final Paragraph paragraph = visualizeParagraphElement(element);
+		return nodeReferenceWithText(element, frame(paragraph, Margin.NULL, Border.NULL, new Padding(5, 4), null));
+	}
+
+	private Paragraph visualizeParagraphElement(final IElement element) {
+		if (element.hasChildren()) {
+			return visualizeElementWithChildren(element);
+		} else {
+			return visualizeEmptyElement(element);
+		}
+	}
+
+	private Paragraph visualizeElementWithChildren(final IElement element) {
+		final Paragraph paragraph = paragraph();
+		visualizeChildrenInline(element.children(), paragraph);
+		return paragraph;
+	}
+
+	private Paragraph visualizeEmptyElement(final IElement element) {
+		final Paragraph paragraph = paragraph();
+		paragraph.appendChild(staticText(" ", TIMES_NEW_ROMAN, Color.BLACK));
+		return paragraph;
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/StructureElementVisualization.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/StructureElementVisualization.java
new file mode 100644
index 0000000..564914c
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/StructureElementVisualization.java
@@ -0,0 +1,28 @@
+/*******************************************************************************
+ * 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.visualization;
+
+import static org.eclipse.vex.core.internal.boxes.BoxFactory.frame;
+import static org.eclipse.vex.core.internal.boxes.BoxFactory.nodeReference;
+import static org.eclipse.vex.core.internal.boxes.BoxFactory.verticalBlock;
+
+import org.eclipse.vex.core.internal.boxes.Border;
+import org.eclipse.vex.core.internal.boxes.IStructuralBox;
+import org.eclipse.vex.core.internal.boxes.Margin;
+import org.eclipse.vex.core.internal.boxes.Padding;
+import org.eclipse.vex.core.provisional.dom.IElement;
+
+public final class StructureElementVisualization extends NodeVisualization<IStructuralBox> {
+	@Override
+	public IStructuralBox visit(final IElement element) {
+		return nodeReference(element, frame(visualizeChildrenStructure(element.children(), verticalBlock()), Margin.NULL, Border.NULL, new Padding(3, 3), null));
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/TextVisualization.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/TextVisualization.java
new file mode 100644
index 0000000..2487556
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/TextVisualization.java
@@ -0,0 +1,28 @@
+/*******************************************************************************
+ * 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.visualization;
+
+import static org.eclipse.vex.core.internal.boxes.BoxFactory.textContent;
+
+import org.eclipse.vex.core.internal.boxes.IInlineBox;
+import org.eclipse.vex.core.internal.core.Color;
+import org.eclipse.vex.core.internal.core.FontSpec;
+import org.eclipse.vex.core.provisional.dom.IText;
+
+public final class TextVisualization extends NodeVisualization<IInlineBox> {
+
+	private static final FontSpec TIMES_NEW_ROMAN = new FontSpec("Times New Roman", FontSpec.PLAIN, 20.0f);
+
+	@Override
+	public IInlineBox visit(final IText text) {
+		return textContent(text.getContent(), text.getRange(), TIMES_NEW_ROMAN, Color.BLACK);
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/VisualizationChain.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/VisualizationChain.java
new file mode 100644
index 0000000..c12af46
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/VisualizationChain.java
@@ -0,0 +1,71 @@
+/*******************************************************************************
+ * 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.visualization;
+
+import java.util.TreeSet;
+
+import org.eclipse.vex.core.internal.boxes.IBox;
+import org.eclipse.vex.core.internal.boxes.IInlineBox;
+import org.eclipse.vex.core.internal.boxes.IStructuralBox;
+import org.eclipse.vex.core.internal.boxes.RootBox;
+import org.eclipse.vex.core.provisional.dom.INode;
+
+public final class VisualizationChain implements IBoxModelBuilder {
+	private final TreeSet<NodeVisualization<RootBox>> rootChain = new TreeSet<NodeVisualization<RootBox>>();
+	private final TreeSet<NodeVisualization<IStructuralBox>> structureChain = new TreeSet<NodeVisualization<IStructuralBox>>();
+	private final TreeSet<NodeVisualization<IInlineBox>> inlineChain = new TreeSet<NodeVisualization<IInlineBox>>();
+
+	@Override
+	public RootBox visualizeRoot(final INode node) {
+		final RootBox rootBox = visualize(node, rootChain);
+		if (rootBox == null) {
+			return new RootBox();
+		}
+		return rootBox;
+	}
+
+	@Override
+	public IStructuralBox visualizeStructure(final INode node) {
+		return visualize(node, structureChain);
+	}
+
+	@Override
+	public IInlineBox visualizeInline(final INode node) {
+		return visualize(node, inlineChain);
+	}
+
+	private static <T extends IBox> T visualize(final INode node, final TreeSet<NodeVisualization<T>> chain) {
+		for (final NodeVisualization<T> visualization : chain) {
+			final T box = visualization.visualize(node);
+			if (box != null) {
+				return box;
+			}
+		}
+		return null;
+	}
+
+	public void addForRoot(final NodeVisualization<RootBox> visualization) {
+		add(visualization, rootChain);
+	}
+
+	public void addForStructure(final NodeVisualization<IStructuralBox> visualization) {
+		add(visualization, structureChain);
+	}
+
+	public void addForInline(final NodeVisualization<IInlineBox> visualization) {
+		add(visualization, inlineChain);
+	}
+
+	private <T extends IBox> void add(final NodeVisualization<T> visualization, final TreeSet<NodeVisualization<T>> chain) {
+		chain.add(visualization);
+		visualization.setChain(this);
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/BalancableRange.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/BalancableRange.java
new file mode 100644
index 0000000..a6bb75d
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/BalancableRange.java
@@ -0,0 +1,93 @@
+package org.eclipse.vex.core.internal.widget;
+
+import org.eclipse.vex.core.provisional.dom.ContentRange;
+import org.eclipse.vex.core.provisional.dom.IAxis;
+import org.eclipse.vex.core.provisional.dom.IDocument;
+import org.eclipse.vex.core.provisional.dom.INode;
+import org.eclipse.vex.core.provisional.dom.IParent;
+import org.eclipse.vex.core.provisional.dom.IText;
+
+public class BalancableRange {
+	private final int startOffset;
+	private final int endOffset;
+	private final INode node;
+	private final IDocument document;
+
+	public BalancableRange(final IDocument document, final int mark, final int offset) {
+		this.document = document;
+		startOffset = Math.min(mark, offset);
+		endOffset = Math.max(mark, offset);
+		node = document.findCommonNode(startOffset, endOffset);
+	}
+
+	public ContentRange expand() {
+		final int balancedStart = balanceBackward(startOffset, node);
+		final int balancedEnd = balanceForward(endOffset, node);
+		return new ContentRange(balancedStart, balancedEnd);
+	}
+
+	public ContentRange reduceForward() {
+		final int balancedStart = balanceForward(startOffset, node);
+		final int balancedEnd = balanceForward(endOffset, node);
+		return new ContentRange(balancedStart, balancedEnd);
+	}
+
+	public ContentRange reduceBackward() {
+		final int balancedStart = balanceBackward(startOffset, node);
+		final int balancedEnd = balanceBackward(endOffset, node);
+		return new ContentRange(balancedStart, balancedEnd);
+	}
+
+	private int balanceForward(final int offset, final INode node) {
+		if (getParentForInsertionAt(offset) == node) {
+			return offset;
+		}
+
+		int balancedOffset = moveToNextNode(offset);
+		while (getParentForInsertionAt(balancedOffset) != node) {
+			balancedOffset = document.getChildAt(balancedOffset).getEndOffset() + 1;
+		}
+		return balancedOffset;
+	}
+
+	private int moveToNextNode(final int offset) {
+		final INode nodeAtOffset = getParentForInsertionAt(offset);
+		final IParent parent = nodeAtOffset.getParent();
+		if (parent == null) {
+			return nodeAtOffset.getEndOffset();
+		}
+		final IAxis<? extends INode> siblings = parent.children().after(offset);
+		if (!siblings.isEmpty()) {
+			return siblings.first().getStartOffset();
+		} else {
+			return parent.getEndOffset();
+		}
+	}
+
+	private INode getParentForInsertionAt(final int offset) {
+		final INode node = document.getChildAt(offset);
+		if (offset == node.getStartOffset()) {
+			return node.getParent();
+		} else if (node instanceof IText) {
+			return node.getParent();
+		} else {
+			return node;
+		}
+	}
+
+	private int balanceBackward(final int offset, final INode node) {
+		if (getParentForInsertionAt(offset) == node) {
+			return offset;
+		}
+
+		int balancedOffset = document.getChildAt(offset).getStartOffset();
+		IParent parent = document.getChildAt(balancedOffset).getParent();
+		while (parent != null && parent != node) {
+			balancedOffset = parent.getStartOffset();
+			parent = document.getChildAt(balancedOffset).getParent();
+		}
+
+		return balancedOffset;
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/BalancingSelector.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/BalancingSelector.java
new file mode 100644
index 0000000..a8b10af
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/BalancingSelector.java
@@ -0,0 +1,100 @@
+/*******************************************************************************
+ * 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.widget;
+
+import org.eclipse.vex.core.provisional.dom.ContentRange;
+import org.eclipse.vex.core.provisional.dom.IDocument;
+
+/**
+ * @author Florian Thienel
+ */
+public class BalancingSelector extends BaseSelector {
+
+	private IDocument document;
+
+	public void setDocument(final IDocument document) {
+		this.document = document;
+		setMark(0);
+	}
+
+	@Override
+	public void moveEndTo(final int offset) {
+		if (document == null) {
+			return;
+		}
+		if (offset == getMark()) {
+			setMark(offset);
+		}
+
+		final Movement movement = new Movement(offset);
+		final BalancableRange rawRange = new BalancableRange(document, getMark(), offset);
+
+		if (movement.movingForward && movement.movingTowardMark) {
+			setRange(rawRange.reduceForward());
+			setCaretOffset(getStartOffset());
+		} else if (movement.movingBackward && movement.movingTowardMark) {
+			setRange(rawRange.reduceBackward());
+			setCaretOffset(getEndOffset());
+		} else if (movement.movingForward && movement.movingAwayFromMark) {
+			setRange(rawRange.expand());
+			setCaretOffset(getEndOffset());
+		} else if (movement.movingBackward && movement.movingAwayFromMark) {
+			setRange(rawRange.expand());
+			setCaretOffset(getStartOffset());
+		}
+	}
+
+	@Override
+	public void setEndAbsoluteTo(final int offset) {
+		if (document == null) {
+			return;
+		}
+		if (offset == getMark()) {
+			setMark(offset);
+		}
+
+		final Movement movement = new Movement(offset);
+		final BalancableRange rawRange = new BalancableRange(document, getMark(), offset);
+
+		if (movement.beforeMark) {
+			setRange(rawRange.expand());
+			setCaretOffset(getStartOffset());
+		} else if (movement.afterMark) {
+			setRange(rawRange.expand());
+			setCaretOffset(getEndOffset());
+		}
+	}
+
+	private void setRange(final ContentRange range) {
+		setStartOffset(range.getStartOffset());
+		setEndOffset(range.getEndOffset());
+	}
+
+	private class Movement {
+
+		public final boolean movingForward;
+		public final boolean movingBackward;
+		public final boolean beforeMark;
+		public final boolean afterMark;
+		public final boolean movingTowardMark;
+		public final boolean movingAwayFromMark;
+
+		public Movement(final int offset) {
+			movingForward = offset > getCaretOffset();
+			movingBackward = offset < getCaretOffset();
+			beforeMark = offset < getMark();
+			afterMark = offset > getMark();
+			movingTowardMark = movingForward && beforeMark || movingBackward && afterMark;
+			movingAwayFromMark = movingForward && afterMark || movingBackward && beforeMark;
+		}
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/BaseSelector.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/BaseSelector.java
new file mode 100644
index 0000000..3d6942b
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/BaseSelector.java
@@ -0,0 +1,68 @@
+/*******************************************************************************
+ * 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.widget;
+
+import org.eclipse.vex.core.internal.cursor.IContentSelector;
+import org.eclipse.vex.core.provisional.dom.ContentRange;
+
+/**
+ * @author Florian Thienel
+ */
+public abstract class BaseSelector implements IContentSelector {
+
+	private int mark;
+	private int startOffset;
+	private int endOffset;
+	private int caretOffset;
+
+	protected final int getMark() {
+		return mark;
+	}
+
+	public final void setMark(final int offset) {
+		mark = offset;
+		startOffset = mark;
+		endOffset = mark;
+		caretOffset = mark;
+	}
+
+	public final boolean isActive() {
+		return mark != caretOffset;
+	}
+
+	public final int getStartOffset() {
+		return startOffset;
+	}
+
+	protected final void setStartOffset(final int startOffset) {
+		this.startOffset = startOffset;
+	}
+
+	public final int getEndOffset() {
+		return endOffset;
+	}
+
+	protected final void setEndOffset(final int endOffset) {
+		this.endOffset = endOffset;
+	}
+
+	public final ContentRange getRange() {
+		return new ContentRange(startOffset, endOffset);
+	}
+
+	public final int getCaretOffset() {
+		return caretOffset;
+	}
+
+	protected final void setCaretOffset(final int caretOffset) {
+		this.caretOffset = caretOffset;
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/BaseVexWidget.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/BaseVexWidget.java
index 5ddba79..49a0ff6 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/BaseVexWidget.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/BaseVexWidget.java
@@ -51,7 +51,7 @@
 import org.eclipse.vex.core.internal.layout.LayoutContext;
 import org.eclipse.vex.core.internal.layout.RootBox;
 import org.eclipse.vex.core.internal.layout.VerticalRange;
-import org.eclipse.vex.core.internal.undo.CannotRedoException;
+import org.eclipse.vex.core.internal.undo.CannotApplyException;
 import org.eclipse.vex.core.internal.undo.CannotUndoException;
 import org.eclipse.vex.core.internal.undo.ChangeAttributeEdit;
 import org.eclipse.vex.core.internal.undo.ChangeNamespaceEdit;
@@ -62,6 +62,7 @@
 import org.eclipse.vex.core.internal.undo.InsertCommentEdit;
 import org.eclipse.vex.core.internal.undo.InsertElementEdit;
 import org.eclipse.vex.core.internal.undo.InsertFragmentEdit;
+import org.eclipse.vex.core.internal.undo.InsertLineBreakEdit;
 import org.eclipse.vex.core.internal.undo.InsertProcessingInstructionEdit;
 import org.eclipse.vex.core.internal.undo.InsertTextEdit;
 import org.eclipse.vex.core.provisional.dom.AttributeChangeEvent;
@@ -121,6 +122,7 @@
 	private IDocument document;
 	private StyleSheet styleSheet;
 	private IWhitespacePolicy whitespacePolicy = DEFAULT_POLICY;
+	private ITableModel tableModel;
 
 	private final BoxFactory boxFactory = new CssBoxFactory();
 
@@ -248,7 +250,6 @@
 		styleSheet = null;
 	}
 
-	@Override
 	public void beginWork() {
 		if (beginWorkCount == 0) {
 			beginWorkCaretPosition = getCaretPosition();
@@ -257,7 +258,6 @@
 		beginWorkCount++;
 	}
 
-	@Override
 	public void endWork(final boolean success) {
 		beginWorkCount--;
 		if (beginWorkCount == 0) {
@@ -395,7 +395,7 @@
 	}
 
 	@Override
-	public void deleteNextChar() throws DocumentValidationException, ReadOnlyException {
+	public void deleteForward() throws DocumentValidationException, ReadOnlyException {
 		if (readOnly) {
 			throw new ReadOnlyException("Cannot delete, because the editor is read-only.");
 		}
@@ -429,7 +429,7 @@
 	}
 
 	@Override
-	public void deletePreviousChar() throws DocumentValidationException, ReadOnlyException {
+	public void deleteBackward() throws DocumentValidationException, ReadOnlyException {
 		if (readOnly) {
 			throw new ReadOnlyException("Cannot delete, because the editor is read-only.");
 		}
@@ -484,7 +484,7 @@
 			if (hasSelection()) {
 				// The position has to be moved here, because selectionStart may be invalid after the deletion.
 				final ContentPosition positionAfterDelete = getSelectionStart().moveBy(-1);
-				applyEdit(new DeleteEdit(document, getSelectedRange()), getSelectionEnd().getOffset());
+				applyEdit(new DeleteEdit(document, getSelectedRange(), getSelectionEnd().getOffset()), getSelectionEnd().getOffset());
 				this.moveTo(positionAfterDelete.moveBy(1));
 			}
 		} catch (final DocumentValidationException e) {
@@ -495,7 +495,7 @@
 	private void deleteNextToCaret() {
 		try {
 			final ContentPosition nextToCaret = getCaretPosition();
-			applyEdit(new DeleteEdit(document, new ContentRange(nextToCaret, nextToCaret)), nextToCaret.getOffset());
+			applyEdit(new DeleteEdit(document, new ContentRange(nextToCaret, nextToCaret), nextToCaret.getOffset()), nextToCaret.getOffset());
 			this.moveTo(nextToCaret);
 		} catch (final DocumentValidationException e) {
 			e.printStackTrace(); // This should never happen, because we constrain the selection
@@ -505,7 +505,7 @@
 	private void deleteBeforeCaret() {
 		try {
 			final ContentPosition beforeCaret = getCaretPosition().moveBy(-1);
-			applyEdit(new DeleteEdit(document, new ContentRange(beforeCaret, beforeCaret)), beforeCaret.getOffset() + 1);
+			applyEdit(new DeleteEdit(document, new ContentRange(beforeCaret, beforeCaret), beforeCaret.getOffset() + 1), beforeCaret.getOffset() + 1);
 			this.moveTo(beforeCaret);
 		} catch (final DocumentValidationException e) {
 			e.printStackTrace(); // This should never happen, because we constrain the selection
@@ -980,7 +980,7 @@
 					final String compressedContent = XML.compressWhitespace(text.getText(), false, false, false);
 					final ContentRange originalTextRange = text.getRange();
 					final CompoundEdit compoundEdit = new CompoundEdit();
-					compoundEdit.addEdit(new DeleteEdit(document, originalTextRange));
+					compoundEdit.addEdit(new DeleteEdit(document, originalTextRange, originalTextRange.getStartOffset()));
 					compoundEdit.addEdit(new InsertTextEdit(document, originalTextRange.getStartOffset(), compressedContent));
 					applyEdit(compoundEdit, originalTextRange.getStartOffset());
 				}
@@ -1019,6 +1019,19 @@
 	}
 
 	@Override
+	public void insertLineBreak() throws DocumentValidationException {
+		if (isReadOnly()) {
+			throw new ReadOnlyException("Cannot insert a character, because the editor is read-only.");
+		}
+
+		if (hasSelection()) {
+			deleteSelection();
+		}
+
+		applyEdit(new InsertLineBreakEdit(document, getCaretOffset()), getCaretOffset());
+	}
+
+	@Override
 	public IComment insertComment() throws DocumentValidationException, ReadOnlyException {
 		if (readOnly) {
 			throw new ReadOnlyException("Cannot insert comment, because the editor is read-only.");
@@ -1067,14 +1080,14 @@
 	}
 
 	@Override
-	public void editProcessingInstruction(final String target, final String data) throws CannotRedoException, ReadOnlyException {
+	public void editProcessingInstruction(final String target, final String data) throws CannotApplyException, ReadOnlyException {
 		if (readOnly) {
 			throw new ReadOnlyException("Cannot change processing instruction, because the editor is read-only.");
 		}
 
 		final INode node = getCurrentNode();
 		if (!(node instanceof IProcessingInstruction)) {
-			throw new CannotRedoException("Current node is not a processing instruction");
+			throw new CannotApplyException("Current node is not a processing instruction");
 		}
 
 		boolean success = false;
@@ -1573,12 +1586,12 @@
 	}
 
 	@Override
-	public void redo() throws CannotRedoException, ReadOnlyException {
+	public void redo() throws CannotApplyException, ReadOnlyException {
 		if (readOnly) {
 			throw new ReadOnlyException("Cannot redo, because the editor is read-only.");
 		}
 		if (redoList.isEmpty()) {
-			throw new CannotRedoException();
+			throw new CannotApplyException();
 		}
 		final UndoableAndOffset event = redoList.removeLast();
 		this.moveTo(new ContentPosition(document, event.caretOffset), false);
@@ -1710,6 +1723,11 @@
 		this.document.addDocumentListener(documentListener);
 	}
 
+	@Override
+	public void setDocument(final IDocument document) {
+		setDocument(document, StyleSheet.NULL);
+	}
+
 	/**
 	 * Called by the host component when it gains or loses focus.
 	 *
@@ -1762,6 +1780,16 @@
 	}
 
 	@Override
+	public ITableModel getTableModel() {
+		return tableModel;
+	}
+
+	@Override
+	public void setTableModel(final ITableModel tableModel) {
+		this.tableModel = tableModel;
+	}
+
+	@Override
 	public boolean canSplit() {
 		if (readOnly) {
 			return false;
@@ -1873,6 +1901,16 @@
 	}
 
 	@Override
+	public boolean isDirty() {
+		return false;
+	}
+
+	@Override
+	public void markClean() {
+		// ignore
+	}
+
+	@Override
 	public ContentPosition viewToModel(final int x, final int y) {
 		final Graphics g = hostComponent.createDefaultGraphics();
 		final LayoutContext context = createLayoutContext(g);
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/BoxView.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/BoxView.java
new file mode 100644
index 0000000..a19616e
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/BoxView.java
@@ -0,0 +1,166 @@
+/*******************************************************************************
+ * 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.widget;
+
+import org.eclipse.vex.core.internal.boxes.IBox;
+import org.eclipse.vex.core.internal.boxes.IChildBox;
+import org.eclipse.vex.core.internal.boxes.RootBox;
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+import org.eclipse.vex.core.internal.cursor.Cursor;
+import org.eclipse.vex.core.internal.widget.IRenderer.IRenderStep;
+
+/**
+ * @author Florian Thienel
+ */
+public class BoxView {
+
+	private final int SCROLL_PADDING = Cursor.CARET_BUFFER * 2;
+
+	private final IRenderer renderer;
+	private final IViewPort viewPort;
+	private final Cursor cursor;
+	private RootBox rootBox;
+	private int width;
+
+	public BoxView(final IRenderer renderer, final IViewPort viewPort, final Cursor cursor) {
+		this.renderer = renderer;
+		this.viewPort = viewPort;
+		this.cursor = cursor;
+		rootBox = new RootBox();
+	}
+
+	public void dispose() {
+		rootBox = null;
+	}
+
+	public void setRootBox(final RootBox rootBox) {
+		this.rootBox = rootBox;
+	}
+
+	public void setWidth(final int width) {
+		this.width = width;
+	}
+
+	public void invalidateLayout(final IBox box) {
+		render(reconcileLayout(box), paintContent());
+	}
+
+	public void invalidateViewport() {
+		render(paintContent());
+	}
+
+	public void invalidateCursor() {
+		render(renderCursorMovement(), paintContent());
+	}
+
+	public void invalidateWidth(final int width) {
+		this.width = width;
+		invalidateEverything();
+	}
+
+	public void invalidateEverything() {
+		final int width = this.width;
+		final IRenderStep invalidateWidth = new IRenderStep() {
+			@Override
+			public void render(final Graphics graphics) {
+				rootBox.setWidth(width);
+			}
+		};
+
+		render(invalidateWidth, layoutContent(), paintContent());
+	}
+
+	private void render(final IRenderStep... steps) {
+		renderer.render(viewPort.getVisibleArea(), steps);
+	}
+
+	private IRenderStep paintContent() {
+		return new IRenderStep() {
+			@Override
+			public void render(final Graphics graphics) {
+				rootBox.paint(graphics);
+				cursor.paint(graphics);
+			}
+		};
+	}
+
+	private IRenderStep layoutContent() {
+		return new IRenderStep() {
+			@Override
+			public void render(final Graphics graphics) {
+				rootBox.layout(graphics);
+				cursor.reconcile(graphics);
+				reconcileViewPort();
+			}
+
+		};
+	}
+
+	private IRenderStep reconcileLayout(final IBox box) {
+		return new IRenderStep() {
+			@Override
+			public void render(final Graphics graphics) {
+				box.layout(graphics);
+				reconcileParentsLayout(box, graphics);
+				reconcileViewPort();
+			}
+		};
+	}
+
+	private void reconcileViewPort() {
+		viewPort.reconcile(rootBox.getHeight() + Cursor.CARET_BUFFER);
+	}
+
+	private void reconcileParentsLayout(final IBox box, final Graphics graphics) {
+		IBox parentBox = getParentBox(box);
+		while (parentBox != null && parentBox.reconcileLayout(graphics)) {
+			parentBox = getParentBox(parentBox);
+		}
+	}
+
+	private IBox getParentBox(final IBox box) {
+		if (box instanceof IChildBox) {
+			return ((IChildBox) box).getParent();
+		}
+		return null;
+	}
+
+	private IRenderStep renderCursorMovement() {
+		return new IRenderStep() {
+			@Override
+			public void render(final Graphics graphics) {
+				cursor.applyMoves(graphics);
+				moveViewPortToCursor(graphics);
+			}
+		};
+	}
+
+	private void moveViewPortToCursor(final Graphics graphics) {
+		final int delta = getDeltaIntoVisibleArea(viewPort.getVisibleArea());
+		graphics.moveOrigin(0, -delta);
+		viewPort.moveRelative(delta);
+	}
+
+	private int getDeltaIntoVisibleArea(final Rectangle visibleArea) {
+		final int top = visibleArea.getY();
+		final int height = visibleArea.getHeight();
+		final int delta = cursor.getDeltaIntoVisibleArea(top, height);
+		if (delta < 0) {
+			return delta - Math.min(SCROLL_PADDING, top + delta);
+		} else if (delta > 0) {
+			return delta + Math.min(SCROLL_PADDING, rootBox.getHeight() + Cursor.CARET_BUFFER - top - height - delta);
+		} else {
+			return delta;
+		}
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/CssTableModel.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/CssTableModel.java
new file mode 100644
index 0000000..c09967a
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/CssTableModel.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.css.StyleSheet;
+
+/**
+ * @author Florian Thienel
+ */
+public class CssTableModel implements ITableModel {
+
+	private final StyleSheet styleSheet;
+
+	public CssTableModel(final StyleSheet styleSheet) {
+		this.styleSheet = styleSheet;
+	}
+
+	@Override
+	public StyleSheet getStyleSheet() {
+		return styleSheet;
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/DOMVisualization.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/DOMVisualization.java
new file mode 100644
index 0000000..eedf7a1
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/DOMVisualization.java
@@ -0,0 +1,191 @@
+/*******************************************************************************
+ * 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.widget;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.eclipse.core.runtime.Assert;
+import org.eclipse.vex.core.internal.boxes.BaseBoxVisitor;
+import org.eclipse.vex.core.internal.boxes.IBox;
+import org.eclipse.vex.core.internal.boxes.IContentBox;
+import org.eclipse.vex.core.internal.boxes.InlineContainer;
+import org.eclipse.vex.core.internal.boxes.InlineFrame;
+import org.eclipse.vex.core.internal.boxes.InlineNodeReference;
+import org.eclipse.vex.core.internal.boxes.List;
+import org.eclipse.vex.core.internal.boxes.ListItem;
+import org.eclipse.vex.core.internal.boxes.NodeEndOffsetPlaceholder;
+import org.eclipse.vex.core.internal.boxes.Paragraph;
+import org.eclipse.vex.core.internal.boxes.RootBox;
+import org.eclipse.vex.core.internal.boxes.StructuralFrame;
+import org.eclipse.vex.core.internal.boxes.StructuralNodeReference;
+import org.eclipse.vex.core.internal.boxes.TextContent;
+import org.eclipse.vex.core.internal.boxes.VerticalBlock;
+import org.eclipse.vex.core.internal.cursor.ContentTopology;
+import org.eclipse.vex.core.internal.cursor.Cursor;
+import org.eclipse.vex.core.internal.visualization.IBoxModelBuilder;
+import org.eclipse.vex.core.provisional.dom.ContentRange;
+import org.eclipse.vex.core.provisional.dom.IDocument;
+import org.eclipse.vex.core.provisional.dom.INode;
+
+/**
+ * @author Florian Thienel
+ */
+public class DOMVisualization {
+
+	private final ContentTopology contentTopology = new ContentTopology();
+	private final Cursor cursor;
+	private final BoxView view;
+
+	private IBoxModelBuilder boxModelBuilder;
+	private IDocument document;
+
+	public DOMVisualization(final Cursor cursor, final BoxView view) {
+		this.cursor = cursor;
+		this.view = view;
+	}
+
+	public void setDocument(final IDocument document) {
+		this.document = document;
+		buildAll();
+	}
+
+	public void setBoxModelBuilder(final IBoxModelBuilder boxModelBuilder) {
+		Assert.isNotNull(boxModelBuilder);
+		this.boxModelBuilder = boxModelBuilder;
+		buildAll();
+	}
+
+	public void buildAll() {
+		if (boxModelBuilder == null) {
+			return;
+		}
+		if (document == null) {
+			return;
+		}
+
+		final RootBox rootBox = boxModelBuilder.visualizeRoot(document);
+
+		contentTopology.setRootBox(rootBox);
+		cursor.setRootBox(rootBox);
+		view.setRootBox(rootBox);
+	}
+
+	public void rebuildStructure(final INode node) {
+		final Collection<IContentBox> boxesToReplace = contentTopology.findBoxesForNode(node);
+		final Collection<IBox> affectedParents = parents(boxesToReplace);
+		if (affectedParents.size() > 1) {
+			rebuildStructure(node.getParent());
+			return;
+		}
+		final IBox parentBox = affectedParents.iterator().next();
+
+		replaceModifiedBoxesWithRebuiltVisualization(parentBox, boxesToReplace, node);
+		view.invalidateLayout(parentBox);
+	}
+
+	private static Set<IBox> parents(final Collection<IContentBox> boxes) {
+		final Set<IBox> parents = new HashSet<IBox>();
+		for (final IContentBox box : boxes) {
+			final IBox parent = box.getParent();
+			if (parent != null) {
+				parents.add(parent);
+			}
+		}
+		return parents;
+	}
+
+	private void replaceModifiedBoxesWithRebuiltVisualization(final IBox parent, final Collection<IContentBox> modifiedBoxes, final INode node) {
+		parent.accept(new BaseBoxVisitor() {
+			@Override
+			public void visit(final RootBox box) {
+				box.replaceChildren(modifiedBoxes, boxModelBuilder.visualizeStructure(node));
+			}
+
+			@Override
+			public void visit(final VerticalBlock box) {
+				box.replaceChildren(modifiedBoxes, boxModelBuilder.visualizeStructure(node));
+			}
+
+			@Override
+			public void visit(final StructuralFrame box) {
+				box.setComponent(boxModelBuilder.visualizeStructure(node));
+			}
+
+			@Override
+			public void visit(final StructuralNodeReference box) {
+				box.setComponent(boxModelBuilder.visualizeStructure(node));
+			}
+
+			@Override
+			public void visit(final ListItem box) {
+				box.setComponent(boxModelBuilder.visualizeStructure(node));
+			}
+
+			@Override
+			public void visit(final List box) {
+				box.setComponent(boxModelBuilder.visualizeStructure(node));
+			}
+
+			@Override
+			public void visit(final Paragraph box) {
+				box.replaceChildren(modifiedBoxes, boxModelBuilder.visualizeInline(node));
+			}
+
+			@Override
+			public void visit(final InlineNodeReference box) {
+				box.setComponent(boxModelBuilder.visualizeInline(node));
+			}
+
+			@Override
+			public void visit(final InlineContainer box) {
+				box.replaceChildren(modifiedBoxes, boxModelBuilder.visualizeInline(node));
+			}
+
+			@Override
+			public void visit(final InlineFrame box) {
+				box.setComponent(boxModelBuilder.visualizeInline(node));
+			}
+		});
+	}
+
+	public void rebuildContentRange(final INode node, final ContentRange modifiedRange) {
+		final IContentBox modifiedBox = contentTopology.findBoxForRange(modifiedRange);
+		Assert.isNotNull(modifiedBox, "No box found for range " + modifiedRange);
+
+		modifiedBox.accept(new BaseBoxVisitor() {
+			@Override
+			public void visit(final StructuralNodeReference box) {
+				rebuildStructure(node);
+			}
+
+			@Override
+			public void visit(final InlineNodeReference box) {
+				rebuildStructure(node);
+			}
+
+			@Override
+			public void visit(final TextContent box) {
+				if (modifiedRange.getStartOffset() == modifiedBox.getStartOffset() || modifiedRange.getEndOffset() == modifiedBox.getEndOffset()) {
+					rebuildStructure(node);
+				} else {
+					view.invalidateLayout(modifiedBox);
+				}
+			}
+
+			@Override
+			public void visit(final NodeEndOffsetPlaceholder box) {
+				view.invalidateLayout(modifiedBox);
+			}
+		});
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/DocumentEditor.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/DocumentEditor.java
new file mode 100644
index 0000000..d070683
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/DocumentEditor.java
@@ -0,0 +1,1493 @@
+/*******************************************************************************
+ * 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.eclipse.vex.core.internal.cursor.CursorMoves.by;
+import static org.eclipse.vex.core.internal.cursor.CursorMoves.down;
+import static org.eclipse.vex.core.internal.cursor.CursorMoves.toLineEnd;
+import static org.eclipse.vex.core.internal.cursor.CursorMoves.toLineStart;
+import static org.eclipse.vex.core.internal.cursor.CursorMoves.toNextPage;
+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.toPreviousPage;
+import static org.eclipse.vex.core.internal.cursor.CursorMoves.toPreviousWord;
+import static org.eclipse.vex.core.internal.cursor.CursorMoves.toWordEnd;
+import static org.eclipse.vex.core.internal.cursor.CursorMoves.toWordStart;
+import static org.eclipse.vex.core.internal.cursor.CursorMoves.up;
+
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import org.eclipse.core.runtime.Assert;
+import org.eclipse.core.runtime.QualifiedName;
+import org.eclipse.vex.core.XML;
+import org.eclipse.vex.core.internal.core.ElementName;
+import org.eclipse.vex.core.internal.core.QualifiedNameComparator;
+import org.eclipse.vex.core.internal.css.IWhitespacePolicy;
+import org.eclipse.vex.core.internal.cursor.ICursor;
+import org.eclipse.vex.core.internal.dom.Node;
+import org.eclipse.vex.core.internal.io.XMLFragment;
+import org.eclipse.vex.core.internal.undo.CannotApplyException;
+import org.eclipse.vex.core.internal.undo.CannotUndoException;
+import org.eclipse.vex.core.internal.undo.ChangeAttributeEdit;
+import org.eclipse.vex.core.internal.undo.ChangeNamespaceEdit;
+import org.eclipse.vex.core.internal.undo.CompoundEdit;
+import org.eclipse.vex.core.internal.undo.DefineOffsetEdit;
+import org.eclipse.vex.core.internal.undo.DeleteEdit;
+import org.eclipse.vex.core.internal.undo.DeleteNextCharEdit;
+import org.eclipse.vex.core.internal.undo.DeletePreviousCharEdit;
+import org.eclipse.vex.core.internal.undo.EditProcessingInstructionEdit;
+import org.eclipse.vex.core.internal.undo.EditStack;
+import org.eclipse.vex.core.internal.undo.IUndoableEdit;
+import org.eclipse.vex.core.internal.undo.InsertCommentEdit;
+import org.eclipse.vex.core.internal.undo.InsertElementEdit;
+import org.eclipse.vex.core.internal.undo.InsertFragmentEdit;
+import org.eclipse.vex.core.internal.undo.InsertLineBreakEdit;
+import org.eclipse.vex.core.internal.undo.InsertProcessingInstructionEdit;
+import org.eclipse.vex.core.internal.undo.InsertTextEdit;
+import org.eclipse.vex.core.internal.undo.JoinElementsAtOffsetEdit;
+import org.eclipse.vex.core.provisional.dom.BaseNodeVisitor;
+import org.eclipse.vex.core.provisional.dom.BaseNodeVisitorWithResult;
+import org.eclipse.vex.core.provisional.dom.ContentPosition;
+import org.eclipse.vex.core.provisional.dom.ContentPositionRange;
+import org.eclipse.vex.core.provisional.dom.ContentRange;
+import org.eclipse.vex.core.provisional.dom.DocumentValidationException;
+import org.eclipse.vex.core.provisional.dom.Filters;
+import org.eclipse.vex.core.provisional.dom.IAxis;
+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.IPosition;
+import org.eclipse.vex.core.provisional.dom.IProcessingInstruction;
+import org.eclipse.vex.core.provisional.dom.IText;
+import org.eclipse.vex.core.provisional.dom.IValidator;
+
+public class DocumentEditor implements IDocumentEditor {
+
+	private final ICursor cursor;
+	private final EditStack editStack;
+	private final IClipboard clipboard;
+
+	private IDocument document;
+	private IWhitespacePolicy whitespacePolicy;
+	private ITableModel tableModel;
+	private boolean readOnly;
+
+	public DocumentEditor(final ICursor cursor) {
+		this(cursor, IWhitespacePolicy.NULL);
+	}
+
+	public DocumentEditor(final ICursor cursor, final IWhitespacePolicy whitespacePolicy) {
+		this(cursor, whitespacePolicy, new InMemoryClipboard());
+	}
+
+	public DocumentEditor(final ICursor cursor, final IWhitespacePolicy whitespacePolicy, final IClipboard clipboard) {
+		this.cursor = cursor;
+		this.whitespacePolicy = whitespacePolicy;
+		this.clipboard = clipboard;
+
+		editStack = new EditStack();
+	}
+
+	/*
+	 * Configuration
+	 */
+
+	@Override
+	public IDocument getDocument() {
+		return document;
+	}
+
+	@Override
+	public void setDocument(final IDocument document) {
+		this.document = document;
+		cursor.move(toOffset(document.getRootElement().getStartOffset() + 1));
+	}
+
+	@Override
+	public IWhitespacePolicy getWhitespacePolicy() {
+		return whitespacePolicy;
+	}
+
+	@Override
+	public void setWhitespacePolicy(final IWhitespacePolicy whitespacePolicy) {
+		if (whitespacePolicy == null) {
+			this.whitespacePolicy = IWhitespacePolicy.NULL;
+		} else {
+			this.whitespacePolicy = whitespacePolicy;
+		}
+	}
+
+	@Override
+	public ITableModel getTableModel() {
+		return tableModel;
+	}
+
+	@Override
+	public void setTableModel(final ITableModel tableModel) {
+		this.tableModel = tableModel;
+	}
+
+	@Override
+	public boolean isReadOnly() {
+		return readOnly;
+	}
+
+	@Override
+	public void setReadOnly(final boolean readOnly) {
+		this.readOnly = readOnly;
+	}
+
+	/*
+	 * Undo/Redo
+	 */
+
+	@Override
+	public boolean canRedo() {
+		return editStack.canRedo();
+	}
+
+	@Override
+	public void redo() throws CannotApplyException {
+		final IUndoableEdit edit = editStack.redo();
+		cursor.move(toOffset(edit.getOffsetAfter()));
+	}
+
+	@Override
+	public boolean canUndo() {
+		return editStack.canUndo();
+	}
+
+	@Override
+	public void undo() throws CannotUndoException {
+		final IUndoableEdit edit = editStack.undo();
+		cursor.move(toOffset(edit.getOffsetBefore()));
+	}
+
+	private <T extends IUndoableEdit> T apply(final T edit) throws CannotApplyException {
+		final T result = editStack.apply(edit);
+		cursor.move(toOffset(result.getOffsetAfter()));
+		return result;
+	}
+
+	public boolean isDirty() {
+		return editStack.isDirty();
+	}
+
+	public void markClean() {
+		editStack.markClean();
+	}
+
+	/*
+	 * Transaction Handling
+	 */
+
+	@Override
+	public void doWork(final Runnable runnable) throws CannotApplyException {
+		doWork(runnable, false);
+	}
+
+	@Override
+	public void doWork(final Runnable runnable, final boolean savePosition) throws CannotApplyException {
+		final IPosition position = document.createPosition(cursor.getOffset());
+		editStack.beginWork();
+		try {
+			runnable.run();
+			editStack.commitWork();
+		} catch (final CannotApplyException e) {
+			final IUndoableEdit work = editStack.rollbackWork();
+			if (work.getOffsetBefore() > 0) {
+				cursor.move(toOffset(work.getOffsetBefore()));
+			}
+			throw e;
+		} catch (final Throwable t) {
+			final IUndoableEdit work = editStack.rollbackWork();
+			if (work.getOffsetBefore() > 0) {
+				cursor.move(toOffset(work.getOffsetBefore()));
+			}
+			// TODO throw exception? at least log error?
+		} finally {
+			if (savePosition) {
+				cursor.move(toOffset(position.getOffset()));
+			}
+			document.removePosition(position);
+		}
+	}
+
+	@Override
+	public void savePosition(final Runnable runnable) {
+		final IPosition position = document.createPosition(cursor.getOffset());
+		try {
+			runnable.run();
+		} finally {
+			cursor.move(toOffset(position.getOffset()));
+			document.removePosition(position);
+		}
+	}
+
+	/*
+	 * Clipboard cut/copy/paste
+	 */
+
+	@Override
+	public void cutSelection() {
+		if (isReadOnly()) {
+			throw new ReadOnlyException("Cannot cut selection, because the editor is read-only.");
+		}
+		clipboard.cutSelection(this);
+	}
+
+	@Override
+	public void copySelection() {
+		clipboard.copySelection(this);
+	}
+
+	@Override
+	public boolean canPaste() {
+		return clipboard.hasContent();
+	}
+
+	@Override
+	public void paste() throws DocumentValidationException {
+		if (isReadOnly()) {
+			throw new ReadOnlyException("Cannot paste, because the editor is read-only.");
+		}
+		clipboard.paste(this);
+	}
+
+	@Override
+	public boolean canPasteText() {
+		return clipboard.hasTextContent();
+	}
+
+	@Override
+	public void pasteText() throws DocumentValidationException {
+		if (isReadOnly()) {
+			throw new ReadOnlyException("Cannot paste text, because the editor is read-only.");
+		}
+		clipboard.pasteText(this);
+	}
+
+	/*
+	 * Caret and Selection
+	 */
+
+	@Override
+	public ContentPosition getCaretPosition() {
+		final INode currentNode = getCurrentNode();
+		return new ContentPosition(currentNode, cursor.getOffset());
+	}
+
+	@Override
+	public IElement getCurrentElement() {
+		return getCurrentNode().accept(new BaseNodeVisitorWithResult<IElement>(null) {
+			@Override
+			public IElement visit(final IElement element) {
+				return element;
+			}
+
+			@Override
+			public IElement visit(final IComment comment) {
+				return comment.getParent().accept(this);
+			}
+
+			@Override
+			public IElement visit(final IText text) {
+				return text.getParent().accept(this);
+			}
+
+			@Override
+			public IElement visit(final IProcessingInstruction pi) {
+				return pi.getParent().accept(this);
+			}
+		});
+	}
+
+	@Override
+	public INode getCurrentNode() {
+		final INode currentNode = document.getNodeForInsertionAt(cursor.getOffset());
+		if (currentNode == null) {
+			return document;
+		}
+		return currentNode;
+	}
+
+	@Override
+	public boolean hasSelection() {
+		return cursor.hasSelection();
+	}
+
+	@Override
+	public ContentRange getSelectedRange() {
+		return cursor.getSelectedRange();
+	}
+
+	@Override
+	public ContentPositionRange getSelectedPositionRange() {
+		final ContentRange selectedRange = getSelectedRange();
+		return new ContentPositionRange(new ContentPosition(document, selectedRange.getStartOffset()), new ContentPosition(document, selectedRange.getEndOffset()));
+	}
+
+	@Override
+	public IDocumentFragment getSelectedFragment() {
+		if (hasSelection()) {
+			return document.getFragment(getSelectedRange().resizeBy(0, -1));
+		} else {
+			return null;
+		}
+	}
+
+	@Override
+	public String getSelectedText() {
+		if (hasSelection()) {
+			return document.getText(getSelectedRange());
+		} else {
+			return "";
+		}
+	}
+
+	@Override
+	public void selectAll() {
+		cursor.move(toOffset(document.getStartOffset() + 1));
+		cursor.select(toOffset(document.getEndOffset() - 1));
+	}
+
+	@Override
+	public void selectWord() {
+		cursor.move(toWordStart());
+		cursor.select(toWordEnd());
+	}
+
+	@Override
+	public void selectContentOf(final INode node) {
+		if (node.isEmpty()) {
+			cursor.move(toOffset(node.getEndOffset()));
+		} else {
+			cursor.move(toOffset(node.getStartOffset() + 1));
+			cursor.select(toOffset(node.getEndOffset()));
+		}
+	}
+
+	@Override
+	public void select(final INode node) {
+		cursor.move(toOffset(node.getStartOffset()));
+		cursor.select(toOffset(node.getEndOffset()));
+	}
+
+	@Override
+	public boolean canDeleteSelection() {
+		if (isReadOnly()) {
+			return false;
+		}
+		if (!hasSelection()) {
+			return false;
+		}
+		return document.canDelete(getSelectedRange().resizeBy(0, -1));
+	}
+
+	@Override
+	public void deleteSelection() {
+		if (isReadOnly()) {
+			throw new ReadOnlyException("Cannot delete, because the editor is read-only.");
+		}
+		if (!hasSelection()) {
+			return;
+		}
+
+		apply(new DeleteEdit(document, getSelectedRange().resizeBy(0, -1), cursor.getOffset()));
+	}
+
+	/*
+	 * Caret Movement
+	 */
+
+	@Override
+	public void moveBy(final int distance) {
+		moveBy(distance, false);
+	}
+
+	@Override
+	public void moveBy(final int distance, final boolean select) {
+		if (select) {
+			cursor.select(by(distance));
+		} else {
+			cursor.move(by(distance));
+		}
+	}
+
+	@Override
+	public void moveTo(final ContentPosition position) {
+		moveTo(position, false);
+	}
+
+	@Override
+	public void moveTo(final ContentPosition position, final boolean select) {
+		if (select) {
+			cursor.select(toOffset(position.getOffset()));
+		} else {
+			cursor.move(toOffset(position.getOffset()));
+		}
+	}
+
+	@Override
+	public void moveToLineEnd(final boolean select) {
+		if (select) {
+			cursor.select(toLineEnd());
+		} else {
+			cursor.move(toLineEnd());
+		}
+	}
+
+	@Override
+	public void moveToLineStart(final boolean select) {
+		if (select) {
+			cursor.select(toLineStart());
+		} else {
+			cursor.move(toLineStart());
+		}
+	}
+
+	@Override
+	public void moveToNextLine(final boolean select) {
+		if (select) {
+			cursor.select(down());
+		} else {
+			cursor.move(down());
+		}
+	}
+
+	@Override
+	public void moveToNextPage(final boolean select) {
+		if (select) {
+			cursor.select(toNextPage());
+		} else {
+			cursor.move(toNextPage());
+		}
+	}
+
+	@Override
+	public void moveToNextWord(final boolean select) {
+		if (select) {
+			cursor.select(toNextWord());
+		} else {
+			cursor.move(toNextWord());
+		}
+	}
+
+	@Override
+	public void moveToPreviousLine(final boolean select) {
+		if (select) {
+			cursor.select(up());
+		} else {
+			cursor.move(up());
+		}
+	}
+
+	@Override
+	public void moveToPreviousPage(final boolean select) {
+		if (select) {
+			cursor.select(toPreviousPage());
+		} else {
+			cursor.move(toPreviousPage());
+		}
+	}
+
+	@Override
+	public void moveToPreviousWord(final boolean select) {
+		if (select) {
+			cursor.select(toPreviousWord());
+		} else {
+			cursor.move(toPreviousWord());
+		}
+	}
+
+	/*
+	 * Namespaces
+	 */
+
+	@Override
+	public void declareNamespace(final String namespacePrefix, final String namespaceURI) {
+		if (isReadOnly()) {
+			throw new ReadOnlyException(MessageFormat.format("Cannot declare namespace {0}, because the editor is read-only.", namespacePrefix));
+		}
+
+		final IElement element = getCurrentElement();
+		if (element == null) {
+			return;
+		}
+		final String currentNamespaceURI = element.getNamespaceURI(namespacePrefix);
+		apply(new ChangeNamespaceEdit(document, cursor.getOffset(), namespacePrefix, currentNamespaceURI, namespaceURI));
+	}
+
+	@Override
+	public void removeNamespace(final String namespacePrefix) {
+		if (isReadOnly()) {
+			throw new ReadOnlyException(MessageFormat.format("Cannot remove namespace {0}, because the editor is read-only.", namespacePrefix));
+		}
+
+		final IElement element = getCurrentElement();
+		if (element == null) {
+			return;
+		}
+		final String currentNamespaceURI = element.getNamespaceURI(namespacePrefix);
+		apply(new ChangeNamespaceEdit(document, cursor.getOffset(), namespacePrefix, currentNamespaceURI, null));
+	}
+
+	@Override
+	public void declareDefaultNamespace(final String namespaceURI) {
+		if (isReadOnly()) {
+			throw new ReadOnlyException("Cannot declare default namespace, because the editor is read-only.");
+		}
+
+		final IElement element = getCurrentElement();
+		if (element == null) {
+			return;
+		}
+		final String currentNamespaceURI = element.getDefaultNamespaceURI();
+		apply(new ChangeNamespaceEdit(document, cursor.getOffset(), null, currentNamespaceURI, namespaceURI));
+	}
+
+	@Override
+	public void removeDefaultNamespace() {
+		if (isReadOnly()) {
+			throw new ReadOnlyException("Cannot remove default namespace, because the editor is read-only.");
+		}
+
+		final IElement element = getCurrentElement();
+		if (element == null) {
+			return;
+		}
+		final String currentNamespaceURI = element.getDefaultNamespaceURI();
+		apply(new ChangeNamespaceEdit(document, cursor.getOffset(), null, currentNamespaceURI, null));
+	}
+
+	/*
+	 * Attributes
+	 */
+
+	@Override
+	public boolean canSetAttribute(final String attributeName, final String value) {
+		if (isReadOnly()) {
+			return false;
+		}
+		final IElement element = getCurrentElement();
+		if (element == null) {
+			return false;
+		}
+		final QualifiedName qualifiedAttributeName = element.qualify(attributeName);
+		return element.canSetAttribute(qualifiedAttributeName, value);
+	}
+
+	@Override
+	public void setAttribute(final String attributeName, final String value) {
+		if (isReadOnly()) {
+			throw new ReadOnlyException(MessageFormat.format("Cannot set attribute {0}, because the editor is read-only.", attributeName));
+		}
+
+		final IElement element = getCurrentElement();
+		if (element == null) {
+			return;
+		}
+
+		final QualifiedName qualifiedAttributeName = element.qualify(attributeName);
+		final String currentAttributeValue = element.getAttributeValue(qualifiedAttributeName);
+		if (value == null) {
+			removeAttribute(attributeName);
+		} else if (!value.equals(currentAttributeValue)) {
+			apply(new ChangeAttributeEdit(document, cursor.getOffset(), qualifiedAttributeName, currentAttributeValue, value));
+		}
+	}
+
+	@Override
+	public boolean canRemoveAttribute(final String attributeName) {
+		if (isReadOnly()) {
+			return false;
+		}
+
+		final IElement element = getCurrentElement();
+		if (element == null) {
+			return false;
+		}
+
+		final QualifiedName qualifiedAttributeName = element.qualify(attributeName);
+		return element.canRemoveAttribute(qualifiedAttributeName);
+	}
+
+	@Override
+	public void removeAttribute(final String attributeName) {
+		if (isReadOnly()) {
+			throw new ReadOnlyException(MessageFormat.format("Cannot remove attribute {0}, because the editor is read-only.", attributeName));
+		}
+
+		final IElement element = getCurrentElement();
+		if (element == null) {
+			return;
+		}
+
+		final QualifiedName qualifiedAttributeName = element.qualify(attributeName);
+		final String currentAttributeValue = element.getAttributeValue(qualifiedAttributeName);
+		if (currentAttributeValue != null) {
+			apply(new ChangeAttributeEdit(document, cursor.getOffset(), qualifiedAttributeName, currentAttributeValue, null));
+		}
+	}
+
+	/*
+	 * Content
+	 */
+
+	@Override
+	public boolean canInsertText() {
+		return canReplaceCurrentSelectionWith(IValidator.PCDATA);
+	}
+
+	private boolean canReplaceCurrentSelectionWith(final QualifiedName... nodeNames) {
+		return canInsertAtCurrentSelection(Arrays.asList(nodeNames));
+	}
+
+	private boolean canInsertAtCurrentSelection(final List<QualifiedName> nodeNames) {
+		if (isReadOnly()) {
+			return false;
+		}
+		if (document == null) {
+			return false;
+		}
+
+		final IValidator validator = document.getValidator();
+		if (validator == null) {
+			return true;
+		}
+
+		final ContentRange selectedRange = getSelectedRange();
+
+		final IElement parent = document.getElementForInsertionAt(selectedRange.getStartOffset());
+		final List<QualifiedName> nodesBefore = Node.getNodeNames(parent.children().before(selectedRange.getStartOffset()));
+		final List<QualifiedName> nodesAfter = Node.getNodeNames(parent.children().after(selectedRange.getEndOffset()));
+
+		return validator.isValidSequence(parent.getQualifiedName(), nodesBefore, nodeNames, nodesAfter, true);
+	}
+
+	@Override
+	public void insertChar(final char c) throws DocumentValidationException {
+		if (isReadOnly()) {
+			throw new ReadOnlyException("Cannot insert a character, because the editor is read-only.");
+		}
+
+		if (hasSelection()) {
+			deleteSelection();
+		}
+
+		apply(new InsertTextEdit(document, cursor.getOffset(), Character.toString(c)));
+	}
+
+	@Override
+	public void insertLineBreak() throws DocumentValidationException {
+		if (isReadOnly()) {
+			throw new ReadOnlyException("Cannot insert a character, because the editor is read-only.");
+		}
+
+		if (hasSelection()) {
+			deleteSelection();
+		}
+
+		apply(new InsertLineBreakEdit(document, cursor.getOffset()));
+	}
+
+	@Override
+	public void deleteForward() throws DocumentValidationException {
+		if (isReadOnly()) {
+			throw new ReadOnlyException("Cannot delete, because the editor is read-only.");
+		}
+
+		if (hasSelection()) {
+			deleteSelection();
+			return;
+		}
+
+		final IUndoableEdit edit;
+		final int offset = cursor.getOffset();
+
+		if (offset == document.getLength()) {
+			// ignore
+			edit = null;
+		} else if (JoinElementsAtOffsetEdit.isBetweenMatchingElements(document, offset)) {
+			edit = new JoinElementsAtOffsetEdit(document, offset);
+		} else if (JoinElementsAtOffsetEdit.isBetweenMatchingElements(document, offset + 1)) {
+			edit = new JoinElementsAtOffsetEdit(document, offset);
+		} else if (document.getNodeForInsertionAt(offset).isEmpty()) {
+			final ContentRange range = document.getNodeForInsertionAt(offset).getRange();
+			edit = new DeleteEdit(document, range, offset);
+		} else if (document.getNodeForInsertionAt(offset + 1).isEmpty()) {
+			final ContentRange range = document.getNodeForInsertionAt(offset + 1).getRange();
+			edit = new DeleteEdit(document, range, offset);
+		} else if (!document.isTagAt(offset)) {
+			edit = new DeleteNextCharEdit(document, offset);
+		} else {
+			edit = null;
+		}
+
+		if (edit == null) {
+			return;
+		}
+
+		apply(edit);
+	}
+
+	@Override
+	public void deleteBackward() throws DocumentValidationException {
+		if (isReadOnly()) {
+			throw new ReadOnlyException("Cannot delete, because the editor is read-only.");
+		}
+
+		if (hasSelection()) {
+			deleteSelection();
+			return;
+		}
+
+		final IUndoableEdit edit;
+		final int offset = cursor.getOffset();
+
+		if (offset == 1) {
+			//ignore
+			edit = null;
+		} else if (JoinElementsAtOffsetEdit.isBetweenMatchingElements(document, offset)) {
+			edit = new JoinElementsAtOffsetEdit(document, offset);
+		} else if (JoinElementsAtOffsetEdit.isBetweenMatchingElements(document, offset - 1)) {
+			edit = new JoinElementsAtOffsetEdit(document, offset);
+		} else if (document.getNodeForInsertionAt(offset).isEmpty()) {
+			final ContentRange range = document.getNodeForInsertionAt(offset).getRange();
+			edit = new DeleteEdit(document, range, offset);
+		} else if (document.getNodeForInsertionAt(offset - 1).isEmpty()) {
+			final ContentRange range = document.getNodeForInsertionAt(offset - 1).getRange();
+			edit = new DeleteEdit(document, range, offset);
+		} else if (!document.isTagAt(offset - 1)) {
+			edit = new DeletePreviousCharEdit(document, offset);
+		} else {
+			edit = null;
+		}
+
+		if (edit == null) {
+			return;
+		}
+
+		apply(edit);
+	}
+
+	@Override
+	public void insertText(final String text) throws DocumentValidationException {
+		if (isReadOnly()) {
+			throw new ReadOnlyException("Cannot insert text, because the editor is read-only.");
+		}
+
+		if (hasSelection()) {
+			deleteSelection();
+		}
+
+		final IElement element = document.getElementForInsertionAt(cursor.getOffset());
+		final boolean isPreformatted = whitespacePolicy.isPre(element);
+
+		final String toInsert;
+		if (!isPreformatted) {
+			toInsert = XML.compressWhitespace(XML.normalizeNewlines(text), true, true, true);
+		} else {
+			toInsert = text;
+		}
+
+		doWork(new Runnable() {
+			@Override
+			public void run() {
+				int i = 0;
+				for (;;) {
+					final int nextLineBreak = toInsert.indexOf('\n', i);
+					if (nextLineBreak == -1) {
+						break;
+					}
+					if (nextLineBreak - i > 0) {
+						apply(new InsertTextEdit(document, cursor.getOffset(), toInsert.substring(i, nextLineBreak)));
+					}
+
+					if (isPreformatted) {
+						apply(new InsertLineBreakEdit(document, cursor.getOffset()));
+					} else {
+						split();
+					}
+					i = nextLineBreak + 1;
+				}
+
+				if (i < toInsert.length()) {
+					apply(new InsertTextEdit(document, cursor.getOffset(), toInsert.substring(i)));
+				}
+			}
+		});
+	}
+
+	@Override
+	public void insertXML(final String xml) throws DocumentValidationException {
+		if (isReadOnly()) {
+			throw new ReadOnlyException("Cannot insert text, because the editor is read-only.");
+		}
+
+		final XMLFragment wrappedFragment = new XMLFragment(xml);
+
+		// If fragment contains only simple Text, use insertText to ensure consistent behavior
+		if (wrappedFragment.isTextOnly()) {
+			insertText(wrappedFragment.getXML());
+			return;
+		}
+
+		final IElement element = getBlockForInsertionAt(cursor.getOffset());
+		final boolean isPreformatted = whitespacePolicy.isPre(element);
+
+		try {
+			final IDocumentFragment fragment = wrappedFragment.getDocumentFragment();
+
+			if (document.canInsertFragment(cursor.getOffset(), fragment)) {
+				insertFragment(fragment);
+			} else if (document.canInsertText(cursor.getOffset())) {
+				insertText(fragment.getText());
+			}
+		} catch (final DocumentValidationException e) {
+			// given XML is not valid - Insert text instead if target is preformatted
+			if (isPreformatted) {
+				insertText(wrappedFragment.getXML());
+			} else {
+				throw e;
+			}
+		}
+	}
+
+	private IElement getBlockForInsertionAt(final int offset) {
+		final IElement element = document.getElementForInsertionAt(offset);
+
+		if (whitespacePolicy.isBlock(element)) {
+			return element;
+		}
+
+		for (final IParent parent : element.ancestors().matching(Filters.elements())) {
+			if (whitespacePolicy.isBlock(parent)) {
+				return (IElement) parent;
+			}
+		}
+
+		return null;
+	}
+
+	/*
+	 * Structure
+	 */
+
+	@Override
+	public ElementName[] getValidInsertElements() {
+		if (isReadOnly()) {
+			return new ElementName[0];
+		}
+		if (document == null) {
+			return new ElementName[0];
+		}
+		final IValidator validator = document.getValidator();
+		if (validator == null) {
+			return new ElementName[0];
+		}
+
+		final ContentRange selectedRange = cursor.getSelectedRange();
+
+		final INode parentNode = document.getNodeForInsertionAt(cursor.getOffset());
+		final boolean parentNodeIsElement = Filters.elements().matches(parentNode);
+		if (!parentNodeIsElement) {
+			return new ElementName[0];
+		}
+
+		final IElement parent = (IElement) parentNode;
+
+		final List<QualifiedName> nodesBefore = Node.getNodeNames(parent.children().before(selectedRange.getStartOffset()));
+		final List<QualifiedName> nodesAfter = Node.getNodeNames(parent.children().after(selectedRange.getEndOffset()));
+		final List<QualifiedName> selectedNodes = Node.getNodeNames(parent.children().in(selectedRange));
+		final List<QualifiedName> candidates = createCandidatesList(validator, parent, IValidator.PCDATA);
+
+		filterInvalidSequences(validator, parent, nodesBefore, nodesAfter, candidates);
+
+		// If there's a selection, root out those candidates that can't contain the selection.
+		if (hasSelection()) {
+			filterInvalidSelectionParents(validator, selectedNodes, candidates);
+		}
+
+		Collections.sort(candidates, new QualifiedNameComparator());
+
+		final ElementName[] result = toElementNames(parent, candidates);
+		return result;
+	}
+
+	private static List<QualifiedName> createCandidatesList(final IValidator validator, final IElement parent, final QualifiedName... exceptions) {
+		final Set<QualifiedName> validItems = validator.getValidItems(parent);
+		final List<QualifiedName> exceptionItems = Arrays.asList(exceptions);
+		final List<QualifiedName> result = new ArrayList<QualifiedName>();
+		for (final QualifiedName validItem : validItems) {
+			if (!exceptionItems.contains(validItem)) {
+				result.add(validItem);
+			}
+		}
+		return result;
+	}
+
+	private static void filterInvalidSequences(final IValidator validator, final IElement parent, final List<QualifiedName> nodesBefore, final List<QualifiedName> nodesAfter, final List<QualifiedName> candidates) {
+		final int sequenceLength = nodesBefore.size() + 1 + nodesAfter.size();
+		for (final Iterator<QualifiedName> iterator = candidates.iterator(); iterator.hasNext();) {
+			final QualifiedName candidate = iterator.next();
+			final List<QualifiedName> sequence = new ArrayList<QualifiedName>(sequenceLength);
+			sequence.addAll(nodesBefore);
+			sequence.add(candidate);
+			sequence.addAll(nodesAfter);
+			if (!canContainContent(validator, parent.getQualifiedName(), sequence)) {
+				iterator.remove();
+			}
+		}
+	}
+
+	private static void filterInvalidSelectionParents(final IValidator validator, final List<QualifiedName> selectedNodes, final List<QualifiedName> candidates) {
+		for (final Iterator<QualifiedName> iter = candidates.iterator(); iter.hasNext();) {
+			final QualifiedName candidate = iter.next();
+			if (!canContainContent(validator, candidate, selectedNodes)) {
+				iter.remove();
+			}
+		}
+	}
+
+	private static boolean canContainContent(final IValidator validator, final QualifiedName elementName, final List<QualifiedName> content) {
+		return validator.isValidSequence(elementName, content, true);
+	}
+
+	private static ElementName[] toElementNames(final IElement parent, final List<QualifiedName> candidates) {
+		final ElementName[] result = new ElementName[candidates.size()];
+		int i = 0;
+		for (final QualifiedName candidate : candidates) {
+			result[i++] = new ElementName(candidate, parent.getNamespacePrefix(candidate.getQualifier()));
+		}
+		return result;
+	}
+
+	@Override
+	public ElementName[] getValidMorphElements() {
+		final IElement currentElement = document.getElementForInsertionAt(cursor.getOffset());
+		if (!canMorphElement(currentElement)) {
+			return new ElementName[0];
+		}
+
+		final IValidator validator = document.getValidator();
+		final IElement parent = currentElement.getParentElement();
+		final List<QualifiedName> candidates = createCandidatesList(validator, parent, IValidator.PCDATA, currentElement.getQualifiedName());
+		if (candidates.isEmpty()) {
+			return new ElementName[0];
+		}
+
+		final List<QualifiedName> content = Node.getNodeNames(currentElement.children());
+		final List<QualifiedName> nodesBefore = Node.getNodeNames(parent.children().before(currentElement.getStartOffset()));
+		final List<QualifiedName> nodesAfter = Node.getNodeNames(parent.children().after(currentElement.getEndOffset()));
+
+		for (final Iterator<QualifiedName> iter = candidates.iterator(); iter.hasNext();) {
+			final QualifiedName candidate = iter.next();
+			if (!canContainContent(validator, candidate, content)) {
+				iter.remove();
+			} else if (!isValidChild(validator, parent.getQualifiedName(), candidate, nodesBefore, nodesAfter)) {
+				iter.remove();
+			}
+		}
+
+		Collections.sort(candidates, new QualifiedNameComparator());
+		return toElementNames(parent, candidates);
+	}
+
+	private boolean canMorphElement(final IElement element) {
+		if (isReadOnly()) {
+			return false;
+		}
+		if (document == null) {
+			return false;
+		}
+		if (document.getValidator() == null) {
+			return false;
+		}
+		if (element.getParentElement() == null) {
+			return false;
+		}
+		if (element == document.getRootElement()) {
+			return false;
+		}
+
+		return true;
+	}
+
+	private static boolean isValidChild(final IValidator validator, final QualifiedName parentName, final QualifiedName elementName, final List<QualifiedName> nodesBefore, final List<QualifiedName> nodesAfter) {
+		return validator.isValidSequence(parentName, nodesBefore, Arrays.asList(elementName), nodesAfter, true);
+	}
+
+	@Override
+	public boolean canInsertElement(final QualifiedName elementName) {
+		return canReplaceCurrentSelectionWith(elementName);
+	}
+
+	@Override
+	public IElement insertElement(final QualifiedName elementName) throws DocumentValidationException {
+		if (isReadOnly()) {
+			throw new ReadOnlyException(MessageFormat.format("Cannot insert element {0}, because the editor is read-only.", elementName));
+		}
+
+		final ContentRange selectedRange = getSelectedRange();
+		final IDocumentFragment selectedFragment;
+		if (hasSelection()) {
+			selectedFragment = getSelectedFragment();
+		} else {
+			selectedFragment = null;
+		}
+
+		final IElement[] result = new IElement[1];
+		doWork(new Runnable() {
+			@Override
+			public void run() {
+				final int offsetForInsertion;
+				if (hasSelection()) {
+					offsetForInsertion = selectedRange.getStartOffset();
+					editStack.apply(new DeleteEdit(document, selectedRange.resizeBy(0, -1), cursor.getOffset()));
+				} else {
+					offsetForInsertion = cursor.getOffset();
+				}
+
+				result[0] = editStack.apply(new InsertElementEdit(document, offsetForInsertion, elementName)).getElement();
+
+				if (selectedFragment != null) {
+					editStack.apply(new InsertFragmentEdit(document, result[0].getEndOffset(), selectedFragment));
+				}
+
+				apply(new DefineOffsetEdit(result[0].getEndOffset(), result[0].getEndOffset()));
+			}
+		});
+
+		return result[0];
+	}
+
+	@Override
+	public boolean canInsertComment() {
+		if (isReadOnly()) {
+			return false;
+		}
+		if (document == null) {
+			return false;
+		}
+		return document.canInsertComment(cursor.getOffset());
+	}
+
+	@Override
+	public IComment insertComment() throws DocumentValidationException {
+		if (isReadOnly()) {
+			throw new ReadOnlyException("Cannot insert comment, because the editor is read-only.");
+		}
+		Assert.isTrue(canInsertComment());
+
+		if (hasSelection()) {
+			deleteSelection();
+		}
+
+		return apply(new InsertCommentEdit(document, cursor.getOffset())).getComment();
+	}
+
+	@Override
+	public boolean canInsertProcessingInstruction() {
+		if (isReadOnly()) {
+			return false;
+		}
+		if (document == null) {
+			return false;
+		}
+		return document.canInsertProcessingInstruction(cursor.getOffset(), null);
+	}
+
+	@Override
+	public IProcessingInstruction insertProcessingInstruction(final String target) throws CannotApplyException, ReadOnlyException {
+		if (isReadOnly()) {
+			throw new ReadOnlyException("Cannot insert processing instruction, because the editor is read-only.");
+		}
+		Assert.isTrue(canInsertProcessingInstruction());
+
+		if (hasSelection()) {
+			deleteSelection();
+		}
+
+		return apply(new InsertProcessingInstructionEdit(document, cursor.getOffset(), target)).getProcessingInstruction();
+	}
+
+	@Override
+	public void editProcessingInstruction(final String target, final String data) throws CannotApplyException, ReadOnlyException {
+		if (isReadOnly()) {
+			throw new ReadOnlyException("Cannot change processing instruction, because the editor is read-only.");
+		}
+		final INode node = getCurrentNode();
+		if (!(node instanceof IProcessingInstruction)) {
+			throw new CannotApplyException("Current node is not a processing instruction");
+		}
+
+		apply(new EditProcessingInstructionEdit(document, cursor.getOffset(), target, data));
+	}
+
+	@Override
+	public boolean canInsertFragment(final IDocumentFragment fragment) {
+		return canInsertAtCurrentSelection(fragment.getNodeNames());
+	}
+
+	@Override
+	public void insertFragment(final IDocumentFragment fragment) throws DocumentValidationException {
+		if (isReadOnly()) {
+			throw new ReadOnlyException("Cannot insert fragment, because the editor is read-only");
+		}
+
+		if (hasSelection()) {
+			deleteSelection();
+		}
+
+		final IElement surroundingElement = document.getElementForInsertionAt(cursor.getOffset());
+
+		doWork(new Runnable() {
+			@Override
+			public void run() {
+				final InsertFragmentEdit insertFragment = editStack.apply(new InsertFragmentEdit(document, cursor.getOffset(), fragment));
+				final IPosition finalOffset = document.createPosition(insertFragment.getOffsetAfter());
+
+				applyWhitespacePolicy(surroundingElement);
+
+				apply(new DefineOffsetEdit(insertFragment.getOffsetAfter(), finalOffset.getOffset()));
+				document.removePosition(finalOffset);
+			}
+		});
+	}
+
+	private void applyWhitespacePolicy(final INode node) {
+		node.accept(new BaseNodeVisitor() {
+			@Override
+			public void visit(final IDocument document) {
+				document.children().accept(this);
+			}
+
+			@Override
+			public void visit(final IDocumentFragment fragment) {
+				fragment.children().accept(this);
+			}
+
+			@Override
+			public void visit(final IElement element) {
+				element.children().accept(this);
+			}
+
+			@Override
+			public void visit(final IText text) {
+				final IParent parentElement = text.ancestors().matching(Filters.elements()).first();
+				if (!whitespacePolicy.isPre(parentElement)) {
+					final String compressedContent = XML.compressWhitespace(text.getText(), false, false, false);
+					final ContentRange originalTextRange = text.getRange();
+					final CompoundEdit compoundEdit = new CompoundEdit();
+					compoundEdit.addEdit(new DeleteEdit(document, originalTextRange, originalTextRange.getStartOffset()));
+					compoundEdit.addEdit(new InsertTextEdit(document, originalTextRange.getStartOffset(), compressedContent));
+					apply(compoundEdit);
+				}
+			}
+		});
+	}
+
+	@Override
+	public boolean canUnwrap() {
+		if (isReadOnly()) {
+			return false;
+		}
+		if (document == null) {
+			return false;
+		}
+		final IValidator validator = document.getValidator();
+		if (validator == null) {
+			return false;
+		}
+
+		final IElement element = document.getElementForInsertionAt(cursor.getOffset());
+		final IElement parent = element.getParentElement();
+		if (parent == null) {
+			// can't unwrap the root
+			return false;
+		}
+
+		final List<QualifiedName> nodesBefore = Node.getNodeNames(parent.children().before(element.getStartOffset()));
+		final List<QualifiedName> newNodes = Node.getNodeNames(element.children());
+		final List<QualifiedName> nodesAfter = Node.getNodeNames(parent.children().after(element.getEndOffset()));
+
+		return validator.isValidSequence(parent.getQualifiedName(), nodesBefore, newNodes, nodesAfter, true);
+	}
+
+	@Override
+	public void unwrap() throws DocumentValidationException {
+		if (isReadOnly()) {
+			throw new ReadOnlyException("Cannot unwrap the element, because the editor is read-only.");
+		}
+
+		final IElement currentElement = document.getElementForInsertionAt(cursor.getOffset());
+		if (currentElement == document.getRootElement()) {
+			throw new DocumentValidationException("Cannot unwrap the root element.");
+		}
+
+		final ContentRange elementRange = currentElement.getRange();
+		final IDocumentFragment elementContent;
+		if (currentElement.isEmpty()) {
+			elementContent = null;
+		} else {
+			elementContent = document.getFragment(elementRange.resizeBy(1, -1));
+		}
+
+		doWork(new Runnable() {
+			@Override
+			public void run() {
+				int offsetAfter;
+				offsetAfter = editStack.apply(new DeleteEdit(document, currentElement.getRange(), cursor.getOffset())).getOffsetAfter();
+				if (elementContent != null) {
+					offsetAfter = editStack.apply(new InsertFragmentEdit(document, elementRange.getStartOffset(), elementContent)).getOffsetAfter();
+				}
+				apply(new DefineOffsetEdit(offsetAfter, offsetAfter));
+			}
+		});
+	}
+
+	@Override
+	public boolean canMorph(final QualifiedName elementName) {
+		final IElement currentElement = document.getElementForInsertionAt(cursor.getOffset());
+		if (!canMorphElement(currentElement)) {
+			return false;
+		}
+
+		final IValidator validator = document.getValidator();
+
+		if (!canContainContent(validator, elementName, Node.getNodeNames(currentElement.children()))) {
+			return false;
+		}
+
+		final IElement parent = currentElement.getParentElement();
+		final List<QualifiedName> nodesBefore = Node.getNodeNames(parent.children().before(currentElement.getStartOffset()));
+		final List<QualifiedName> nodesAfter = Node.getNodeNames(parent.children().after(currentElement.getEndOffset()));
+
+		return isValidChild(validator, parent.getQualifiedName(), elementName, nodesBefore, nodesAfter);
+	}
+
+	@Override
+	public void morph(final QualifiedName elementName) throws DocumentValidationException {
+		if (isReadOnly()) {
+			throw new ReadOnlyException(MessageFormat.format("Cannot morph to element {0}, because the editor is read-only.", elementName));
+		}
+
+		final IElement currentElement = document.getElementForInsertionAt(cursor.getOffset());
+		if (currentElement == document.getRootElement()) {
+			throw new DocumentValidationException("Cannot morph the root element.");
+		}
+
+		final ContentRange elementRange = currentElement.getRange();
+		final IDocumentFragment elementContent;
+		if (currentElement.isEmpty()) {
+			elementContent = null;
+		} else {
+			elementContent = document.getFragment(elementRange.resizeBy(1, -1));
+		}
+
+		doWork(new Runnable() {
+			@Override
+			public void run() {
+				editStack.apply(new DeleteEdit(document, currentElement.getRange(), cursor.getOffset()));
+				final InsertElementEdit insertElement = editStack.apply(new InsertElementEdit(document, elementRange.getStartOffset(), elementName));
+				if (elementContent != null) {
+					editStack.apply(new InsertFragmentEdit(document, insertElement.getElement().getEndOffset(), elementContent));
+				}
+				apply(new DefineOffsetEdit(insertElement.getElement().getEndOffset(), insertElement.getElement().getEndOffset()));
+			}
+		});
+	}
+
+	@Override
+	public boolean canJoin() {
+		if (isReadOnly()) {
+			return false;
+		}
+		if (!hasSelection()) {
+			return false;
+		}
+
+		final ContentRange selectedRange = getSelectedRange();
+		final IElement parent = document.getElementForInsertionAt(selectedRange.getStartOffset());
+		final IAxis<? extends INode> selectedNodes = parent.children().in(selectedRange);
+		if (selectedNodes.isEmpty()) {
+			return false;
+		}
+
+		final IValidator validator = document.getValidator();
+		final INode firstNode = selectedNodes.first();
+		final List<QualifiedName> childNodeNames = new ArrayList<QualifiedName>();
+		int count = 0;
+		for (final INode selectedNode : selectedNodes) {
+			if (!selectedNode.isKindOf(firstNode)) {
+				return false;
+			}
+			childNodeNames.addAll(selectedNode.accept(new BaseNodeVisitorWithResult<List<QualifiedName>>(Collections.<QualifiedName> emptyList()) {
+				@Override
+				public List<QualifiedName> visit(final IElement element) {
+					return Node.getNodeNames(element.children());
+				}
+			}));
+			count++;
+		}
+
+		if (count <= 1) {
+			return false;
+		}
+
+		final boolean joinedChildrenValid = firstNode.accept(new BaseNodeVisitorWithResult<Boolean>(true) {
+			@Override
+			public Boolean visit(final IElement element) {
+				return validator.isValidSequence(element.getQualifiedName(), childNodeNames, true);
+			}
+		});
+		if (!joinedChildrenValid) {
+			return false;
+		}
+
+		return true;
+	}
+
+	@Override
+	public void join() throws DocumentValidationException {
+		if (isReadOnly()) {
+			return;
+		}
+		if (!hasSelection()) {
+			return;
+		}
+
+		final ContentRange selectedRange = getSelectedRange();
+		final IElement parent = document.getElementForInsertionAt(selectedRange.getStartOffset());
+		final IAxis<? extends INode> selectedNodes = parent.children().in(selectedRange);
+		if (selectedNodes.isEmpty()) {
+			return;
+		}
+
+		final INode firstNode = selectedNodes.first();
+		final ArrayList<IDocumentFragment> contentToJoin = new ArrayList<IDocumentFragment>();
+		for (final INode selectedNode : selectedNodes) {
+			if (!selectedNode.isKindOf(firstNode) && !contentToJoin.isEmpty()) {
+				throw new DocumentValidationException("Cannot join nodes of different kind.");
+			}
+			if (!selectedNode.isEmpty()) {
+				contentToJoin.add(document.getFragment(selectedNode.getRange().resizeBy(1, -1)));
+			}
+		}
+
+		if (contentToJoin.size() <= 1) {
+			return;
+		}
+
+		doWork(new Runnable() {
+			@Override
+			public void run() {
+				final DeleteEdit deletePreservedContent = editStack
+						.apply(new DeleteEdit(document, new ContentRange(firstNode.getEndOffset() + 1, selectedRange.getEndOffset() - 1), cursor.getOffset()));
+				if (!firstNode.isEmpty()) {
+					editStack.apply(new DeleteEdit(document, firstNode.getRange().resizeBy(1, -1), deletePreservedContent.getOffsetAfter()));
+				}
+				for (final IDocumentFragment contentPart : contentToJoin) {
+					editStack.apply(new InsertFragmentEdit(document, firstNode.getEndOffset(), contentPart));
+				}
+				apply(new DefineOffsetEdit(deletePreservedContent.getOffsetBefore(), firstNode.getEndOffset()));
+			}
+		});
+	}
+
+	@Override
+	public boolean canSplit() {
+		if (isReadOnly()) {
+			return false;
+		}
+		if (document == null) {
+			return false;
+		}
+
+		final IValidator validator = document.getValidator();
+		if (validator == null) {
+			return true;
+		}
+
+		final INode currentNode = getCurrentNode();
+		if (!Filters.elements().matches(currentNode)) {
+			return false;
+		}
+
+		final IElement element = (IElement) currentNode;
+		final IElement parent = element.getParentElement();
+		if (parent == null) {
+			return false;
+		}
+
+		final int startOffset = element.getStartOffset();
+		final int endOffset = element.getEndOffset();
+
+		final List<QualifiedName> nodesBefore = Node.getNodeNames(parent.children().before(startOffset));
+		final List<QualifiedName> newNodes = Arrays.asList(element.getQualifiedName(), element.getQualifiedName());
+		final List<QualifiedName> nodesAfter = Node.getNodeNames(parent.children().after(endOffset));
+
+		return validator.isValidSequence(parent.getQualifiedName(), nodesBefore, newNodes, nodesAfter, true);
+	}
+
+	@Override
+	public void split() throws DocumentValidationException {
+		if (isReadOnly()) {
+			throw new ReadOnlyException("Cannot split, because the editor is read-only.");
+		}
+
+		final INode currentNode = getCurrentNode();
+		if (!Filters.elements().matches(currentNode)) {
+			throw new DocumentValidationException("Can only split elements.");
+		}
+		final IElement element = (IElement) currentNode;
+
+		if (hasSelection()) {
+			deleteSelection();
+		}
+
+		final boolean splitAtEnd = cursor.getOffset() == element.getEndOffset();
+		final ContentRange splittingRange;
+		final IDocumentFragment splittedFragment;
+		if (!splitAtEnd) {
+			splittingRange = new ContentRange(cursor.getOffset(), element.getEndOffset() - 1);
+			splittedFragment = document.getFragment(splittingRange);
+		} else {
+			splittingRange = null;
+			splittedFragment = null;
+		}
+
+		doWork(new Runnable() {
+			@Override
+			public void run() {
+				if (!splitAtEnd) {
+					editStack.apply(new DeleteEdit(document, splittingRange, cursor.getOffset()));
+				}
+				final IElement newElement = editStack.apply(new InsertElementEdit(document, element.getEndOffset() + 1, element.getQualifiedName())).getElement();
+				if (!splitAtEnd) {
+					editStack.apply(new InsertFragmentEdit(document, newElement.getEndOffset(), splittedFragment));
+				}
+				apply(new DefineOffsetEdit(cursor.getOffset(), newElement.getStartOffset() + 1));
+			}
+		});
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/IClipboard.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/IClipboard.java
new file mode 100644
index 0000000..f018f9e
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/IClipboard.java
@@ -0,0 +1,63 @@
+/*******************************************************************************
+ * 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.provisional.dom.DocumentValidationException;
+
+public interface IClipboard {
+
+	void dispose();
+
+	/**
+	 * Cuts the current selection to the clipboard.
+	 *
+	 * @param editor
+	 *            TODO
+	 */
+	void cutSelection(IDocumentEditor editor);
+
+	/**
+	 * Copy the current selection to the clipboard.
+	 *
+	 * @param editor
+	 *            TODO
+	 */
+	void copySelection(IDocumentEditor editor);
+
+	/**
+	 * Returns true if the clipboard has content that can be pasted. Used to enable/disable the paste action of a
+	 * containing application.
+	 */
+	boolean hasContent();
+
+	/**
+	 * Paste the current clipboard contents into the document at the current caret position.
+	 *
+	 * @param editor
+	 *            TODO
+	 */
+	void paste(IDocumentEditor editor) throws DocumentValidationException;
+
+	/**
+	 * Returns true if the clipboard has plain text content that can be pasted. Used to enable/disable the "paste text"
+	 * action of a containing application.
+	 */
+	boolean hasTextContent();
+
+	/**
+	 * Paste the current clipboard contents as plain text into the document at the current caret position.
+	 *
+	 * @param editor
+	 *            TODO
+	 */
+	void pasteText(IDocumentEditor editor) throws DocumentValidationException;
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/IDocumentEditor.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/IDocumentEditor.java
new file mode 100644
index 0000000..2525d89
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/IDocumentEditor.java
@@ -0,0 +1,597 @@
+/*******************************************************************************
+ * 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.widget;
+
+import org.eclipse.core.runtime.QualifiedName;
+import org.eclipse.vex.core.internal.core.ElementName;
+import org.eclipse.vex.core.internal.css.IWhitespacePolicy;
+import org.eclipse.vex.core.internal.undo.CannotApplyException;
+import org.eclipse.vex.core.internal.undo.CannotUndoException;
+import org.eclipse.vex.core.provisional.dom.ContentPosition;
+import org.eclipse.vex.core.provisional.dom.ContentPositionRange;
+import org.eclipse.vex.core.provisional.dom.ContentRange;
+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.IProcessingInstruction;
+
+public interface IDocumentEditor {
+
+	/*
+	 * Configuration
+	 */
+
+	/**
+	 * Returns the document associated with this editor.
+	 */
+	IDocument getDocument();
+
+	/**
+	 * Sets a new document for editing.
+	 *
+	 * @param document
+	 *            new Document to edit
+	 */
+	void setDocument(IDocument document);
+
+	IWhitespacePolicy getWhitespacePolicy();
+
+	void setWhitespacePolicy(IWhitespacePolicy policy);
+
+	ITableModel getTableModel();
+
+	void setTableModel(ITableModel tableModel);
+
+	/**
+	 * @return true if this editor is read-only
+	 */
+	boolean isReadOnly();
+
+	/**
+	 * Make this editor read-only.
+	 *
+	 * @param readOnly
+	 *            set to true if this editor should be read-only
+	 */
+	void setReadOnly(boolean readOnly);
+
+	/*
+	 * Undo/Redo
+	 */
+
+	/**
+	 * Returns true if a redo can be performed.
+	 */
+	boolean canRedo();
+
+	/**
+	 * Redoes the last action on the redo stack.
+	 *
+	 * @throws CannotApplyException
+	 *             if the last action cannot be re-done, or if there is nothing to redo.
+	 */
+	void redo() throws CannotApplyException;
+
+	/**
+	 * Returns true if an undo can be performed.
+	 */
+	boolean canUndo();
+
+	/**
+	 * Undoes the last action on the undo stack.
+	 *
+	 * @throws CannotUndoException
+	 *             if the last action cannot be undone, or if there's nothing left to undo.
+	 */
+	void undo() throws CannotUndoException;
+
+	boolean isDirty();
+
+	void markClean();
+
+	/*
+	 * Transaction Handling
+	 */
+
+	/**
+	 * Perform the runnable's run method within a transaction. All operations in the runnable are treated as a single
+	 * unit of work, and can be undone in one operation by the user. Also, if a later operation fails, all earlier
+	 * operations are also undone.
+	 *
+	 * @param runnable
+	 *            Runnable implementing the work to be done.
+	 */
+	void doWork(Runnable runnable) throws CannotApplyException;
+
+	/**
+	 * Perform the runnable's run method within a transaction. All operations in the runnable are treated as a single
+	 * unit of work, and can be undone in one operation by the user. Also, if a later operation fails, all earlier
+	 * operations are also undone.
+	 *
+	 * @param runnable
+	 *            Runnable implementing the work to be done.
+	 * @param savePosition
+	 *            If true, the current caret position is saved and restored once the operation is complete.
+	 */
+	void doWork(Runnable runnable, boolean savePosition) throws CannotApplyException;
+
+	/**
+	 * Execute a Runnable, restoring the caret position to its original position afterward.
+	 *
+	 * @param runnable
+	 *            Runnable to be invoked.
+	 */
+	void savePosition(Runnable runnable);
+
+	/*
+	 * Clipboard cut/copy/paste
+	 */
+
+	/**
+	 * Cuts the current selection to the clipboard.
+	 */
+	void cutSelection();
+
+	/**
+	 * Copy the current selection to the clipboard.
+	 */
+	void copySelection();
+
+	/**
+	 * Returns true if the clipboard has content that can be pasted. Used to enable/disable the paste action of a
+	 * containing application.
+	 */
+	boolean canPaste();
+
+	/**
+	 * Paste the current clipboard contents into the document at the current caret position.
+	 */
+	void paste() throws DocumentValidationException;
+
+	/**
+	 * Returns true if the clipboard has plain text content that can be pasted. Used to enable/disable the "paste text"
+	 * action of a containing application.
+	 */
+	boolean canPasteText();
+
+	/**
+	 * Paste the current clipboard contents as plain text into the document at the current caret position.
+	 */
+	void pasteText() throws DocumentValidationException;
+
+	/*
+	 * Caret and Selection
+	 */
+
+	/**
+	 * Return the offset into the document represented by the caret.
+	 */
+	ContentPosition getCaretPosition();
+
+	/**
+	 * Returns the element at the current caret offset.
+	 */
+	IElement getCurrentElement();
+
+	/**
+	 * Returns the node a the current caret offset.
+	 */
+	INode getCurrentNode();
+
+	/**
+	 * Returns the offset range in the content which is selected.
+	 */
+	ContentRange getSelectedRange();
+
+	/**
+	 * Returns the {@link ContentPositionRange} which is selected.
+	 */
+	ContentPositionRange getSelectedPositionRange();
+
+	/**
+	 * Returns the currently selected document fragment, or null if there is no current selection.
+	 */
+	IDocumentFragment getSelectedFragment();
+
+	/**
+	 * Returns the currently selected string, or an empty string if there is no current selection.
+	 */
+	String getSelectedText();
+
+	/**
+	 * Returns true if the user currently has some text selected.
+	 */
+	boolean hasSelection();
+
+	/**
+	 * Selects all content in the document.
+	 */
+	void selectAll();
+
+	/**
+	 * Selects the word at the current caret offset.
+	 */
+	void selectWord();
+
+	/**
+	 * Selects the content of the given node.
+	 *
+	 * @param node
+	 *            the node
+	 */
+	void selectContentOf(INode node);
+
+	/**
+	 * Selects the given node.
+	 *
+	 * @param node
+	 *            the node to select
+	 */
+	void select(INode node);
+
+	/**
+	 * @return true if the current selection can be deleted. Returns false if there is no selection.
+	 */
+	boolean canDeleteSelection();
+
+	/**
+	 * Delete the current selection. Does nothing if there is no current selection.
+	 */
+	void deleteSelection();
+
+	/*
+	 * Caret Movement
+	 */
+
+	/**
+	 * Moves the caret a given distance relative to the current caret offset.
+	 *
+	 * @param distance
+	 *            Amount by which to alter the caret offset. Positive values increase the caret offset.
+	 */
+	void moveBy(int distance);
+
+	/**
+	 * Moves the caret a given distance relative to the current caret offset.
+	 *
+	 * @param distance
+	 *            Amount by which to alter the caret offset. Positive values increase the caret offset.
+	 * @param select
+	 *            if true, the current selection is extended to match the new caret offset
+	 */
+	void moveBy(int distance, boolean select);
+
+	/**
+	 * Moves the caret to a new offset. The selection is not extended. This is equivalent to
+	 * <code>moveTo(offset, false)</code>.
+	 *
+	 * @param int
+	 *            new offset for the caret. The offset must be >= 1 and less than the document size; if not, it is
+	 *            silently ignored.
+	 */
+	void moveTo(final ContentPosition position);
+
+	/**
+	 * Moves the caret to the new offset, possibly changing the selection.
+	 *
+	 * @param int
+	 *            new offset for the caret. The offset must be >= 1 and less than the document size; if not, it is
+	 *            silently ignored.
+	 * @param select
+	 *            if true, the current selection is extended to match the new caret offset.
+	 */
+	void moveTo(final ContentPosition position, boolean select);
+
+	/**
+	 * Move the caret to the end of the current line.
+	 *
+	 * @param select
+	 *            If true, the selection is extended.
+	 */
+	void moveToLineEnd(boolean select);
+
+	/**
+	 * Move the caret to the start of the current line.
+	 *
+	 * @param select
+	 *            If true, the selection is extended.
+	 */
+	void moveToLineStart(boolean select);
+
+	/**
+	 * Move the caret down to the next line. Attempts to preserve the same distance from the left edge of the control.
+	 *
+	 * @param select
+	 *            If true, the selection is extended.
+	 */
+	void moveToNextLine(boolean select);
+
+	/**
+	 * Move the caret down to the next page. Attempts to preserve the same distance from the left edge of the control.
+	 *
+	 * @param select
+	 *            If true, the selection is extended.
+	 */
+	void moveToNextPage(boolean select);
+
+	/**
+	 * Moves the caret to the end of the current or next word.
+	 *
+	 * @param select
+	 *            If true, the selection is extended.
+	 */
+	void moveToNextWord(boolean select);
+
+	/**
+	 * Moves the caret up to the previous line.
+	 *
+	 * @param select
+	 *            If true, the selection is extended
+	 */
+	void moveToPreviousLine(boolean select);
+
+	/**
+	 * Moves the caret up to the previous page.
+	 *
+	 * @param select
+	 *            If true, the selection is extended
+	 */
+	void moveToPreviousPage(boolean select);
+
+	/**
+	 * Moves the caret to the start of the current or previous word.
+	 *
+	 * @param select
+	 *            If true, the selection is extended.
+	 */
+	void moveToPreviousWord(boolean select);
+
+	/*
+	 * Namespaces
+	 */
+
+	void declareNamespace(final String namespacePrefix, final String namespaceURI);
+
+	void removeNamespace(final String namespacePrefix);
+
+	void declareDefaultNamespace(final String namespaceURI);
+
+	void removeDefaultNamespace();
+
+	/*
+	 * Attributes
+	 */
+
+	/**
+	 * @param attributeName
+	 *            local name of the attribute being changed.
+	 * @param value
+	 *            New value for the attribute. If null, the attribute is removed from the element.
+	 * @return true if the given value is valid for the attribute with the given name
+	 */
+	boolean canSetAttribute(String attributeName, String value);
+
+	/**
+	 * Sets the value of an attribute in the current element. Attributes set in this manner (as opposed to calling
+	 * Element.setAttribute directly) will be subject to undo/redo.
+	 *
+	 * @param attributeName
+	 *            local name of the attribute being changed.
+	 * @param value
+	 *            New value for the attribute. If null, the attribute is removed from the element.
+	 */
+	void setAttribute(String attributeName, String value);
+
+	/**
+	 * @param attributeName
+	 *            the local name of the attribute to remove
+	 * @return true if it is valid to remove the attribute with the given name
+	 */
+	boolean canRemoveAttribute(String attributeName);
+
+	/**
+	 * Removes an attribute from the current element. Attributes removed in this manner (as opposed to calling
+	 * Element.setAttribute directly) will be subject to undo/redo.
+	 *
+	 * @param attributeName
+	 *            local name of the attribute to remove.
+	 */
+	void removeAttribute(String attributeName);
+
+	/*
+	 * Content
+	 */
+
+	/**
+	 * Returns true if text can be inserted at the current position.
+	 */
+	boolean canInsertText();
+
+	/**
+	 * Inserts the given character at the current caret position. Any selected content is deleted. The main difference
+	 * between this method and insertText is that this method does not use beginWork/endWork, so consecutive calls to
+	 * insertChar are collapsed into a single IUndoableEdit. This method should normally only be called in response to a
+	 * user typing a key.
+	 *
+	 * @param c
+	 *            Character to insert.
+	 */
+	void insertChar(char c) throws DocumentValidationException;
+
+	void insertLineBreak() throws DocumentValidationException;
+
+	/**
+	 * Deletes the character to the right of the caret.
+	 */
+	void deleteForward() throws DocumentValidationException;
+
+	/**
+	 * Deletes the character to the left of the caret.
+	 */
+	void deleteBackward() throws DocumentValidationException;
+
+	/**
+	 * Inserts the given text at the current caret position. Any selected content is first deleted.
+	 *
+	 * @param text
+	 *            String to insert.
+	 */
+	void insertText(String text) throws DocumentValidationException;
+
+	/**
+	 * Inserts the given XML fragment at the current caret position. Any selected content is first deleted.
+	 *
+	 * @param xml
+	 *            XML to insert
+	 * @throws DocumentValidationException
+	 */
+	public void insertXML(String xml) throws DocumentValidationException;
+
+	/*
+	 * Structure
+	 */
+
+	/**
+	 * Returns an array of names of elements that are valid to insert at the given caret offset and selection
+	 */
+	ElementName[] getValidInsertElements();
+
+	/**
+	 * Returns an array of names of elements to which the element at the current caret location can be morphed.
+	 */
+	ElementName[] getValidMorphElements();
+
+	/**
+	 * @param elementName
+	 *            the qualified name of the element to insert
+	 * @return true if an element with the given name can be inserted at the current caret position/instead of the
+	 *         current selection
+	 */
+	boolean canInsertElement(QualifiedName elementName);
+
+	/**
+	 * Inserts the given element at the current caret position. Any selected content becomes the new contents of the
+	 * element.
+	 *
+	 * @param elementName
+	 *            Qualified name of the element to insert.
+	 * @return the newly inserted element
+	 */
+	IElement insertElement(QualifiedName elementName) throws DocumentValidationException;
+
+	/**
+	 * @return true if a comment can be inserted at the current caret position/instead of the current selection
+	 */
+	boolean canInsertComment();
+
+	/**
+	 * Inserts a comment a the current caret position. Any selected content is first deleted.
+	 *
+	 * @return the new comment
+	 */
+	IComment insertComment() throws DocumentValidationException;
+
+	/**
+	 * @return true if a processing instruction can be inserted at the current caret position/instead of the current
+	 *         selection
+	 */
+	boolean canInsertProcessingInstruction();
+
+	/**
+	 * Inserts a processing instruction at the current caret position. Any selected content is first deleted.
+	 *
+	 * @return the new comment
+	 */
+	IProcessingInstruction insertProcessingInstruction(final String target) throws CannotApplyException, ReadOnlyException;
+
+	/**
+	 * Edits the processing instruction at the current caret position. Updates target and data with the given Strings.
+	 *
+	 * @param target
+	 *            The target to set. may be null to keep the old target.
+	 * @param data
+	 *            The data to set. May be null to keep the old value.
+	 */
+	void editProcessingInstruction(final String target, final String data) throws CannotApplyException, ReadOnlyException;
+
+	/**
+	 * Returns true if the given fragment can be inserted at the current caret position.
+	 *
+	 * @param fragment
+	 *            DocumentFragment to be inserted.
+	 */
+	boolean canInsertFragment(final IDocumentFragment fragment);
+
+	/**
+	 * Inserts the given document fragment at the current caret position. Any selected content is deleted.
+	 *
+	 * @param frag
+	 *            DocumentFragment to insert.
+	 */
+	void insertFragment(IDocumentFragment fragment) throws DocumentValidationException;
+
+	/**
+	 * Returns true if the current element can be unwrapped, i.e. replaced with its content.
+	 */
+	boolean canUnwrap();
+
+	void unwrap() throws DocumentValidationException;
+
+	/**
+	 * Indicates whether the current element can be morphed into the given element.
+	 *
+	 * @param elementName
+	 *            Qualified name of the element to morph the current element into.
+	 * @return true if the current element can be morphed
+	 */
+	boolean canMorph(QualifiedName elementName);
+
+	/**
+	 * Replaces the current element with an element with the given name. The content of the element is preserved.
+	 *
+	 * @param elementName
+	 *            Qualified name of the element to replace the current element with.
+	 * @throws DocumentValidationException
+	 *             if the given element is not valid at this place in the document, or if the current element's content
+	 *             is not compatible with the given element.
+	 */
+	void morph(QualifiedName elementName) throws DocumentValidationException;
+
+	/**
+	 * Indiciates whether the caret is at a position between adjacent nodes that can be joined.
+	 *
+	 * @return true if the nodes adjacent to the caret can be joined
+	 */
+	boolean canJoin();
+
+	/**
+	 * Joins the nodes adjacent to the caret.
+	 *
+	 * @throws DocumentValidationException
+	 */
+	void join() throws DocumentValidationException;
+
+	/**
+	 * Indicates whether the current element can be splitted into two elements at the current caret position.
+	 *
+	 * @return true if the current element can be splitted
+	 */
+	boolean canSplit();
+
+	/**
+	 * Splits the element at the current caret offset.
+	 */
+	void split() throws DocumentValidationException;
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/IRenderer.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/IRenderer.java
new file mode 100644
index 0000000..a2f140d
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/IRenderer.java
@@ -0,0 +1,27 @@
+/*******************************************************************************
+ * 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.widget;
+
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+
+/**
+ * @author Florian Thienel
+ */
+public interface IRenderer {
+
+	void render(final Rectangle viewPort, final IRenderStep... steps);
+
+	static interface IRenderStep {
+		void render(Graphics graphics);
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/ITableModel.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/ITableModel.java
new file mode 100644
index 0000000..1a0198f
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/ITableModel.java
@@ -0,0 +1,26 @@
+/*******************************************************************************
+ * 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.css.StyleSheet;
+
+/**
+ * TODO This is only an intermediate solution to provide the stylesheet to all table-related functions. We need an
+ * abstraction of the table model in order to remove the direct dependency to CSS.
+ *
+ * @author Florian Thienel
+ */
+public interface ITableModel {
+
+	@Deprecated
+	StyleSheet getStyleSheet();
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/IVexWidget.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/IVexWidget.java
index 487b896..bb2f0dc 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/IVexWidget.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/IVexWidget.java
@@ -11,39 +11,22 @@
  *******************************************************************************/
 package org.eclipse.vex.core.internal.widget;
 
-import org.eclipse.core.runtime.QualifiedName;
-import org.eclipse.vex.core.internal.core.ElementName;
 import org.eclipse.vex.core.internal.css.IWhitespacePolicy;
 import org.eclipse.vex.core.internal.css.StyleSheet;
-import org.eclipse.vex.core.internal.undo.CannotRedoException;
-import org.eclipse.vex.core.internal.undo.CannotUndoException;
 import org.eclipse.vex.core.provisional.dom.ContentPosition;
-import org.eclipse.vex.core.provisional.dom.ContentPositionRange;
-import org.eclipse.vex.core.provisional.dom.ContentRange;
-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.IProcessingInstruction;
 
 /**
  * Methods implemented by implementations of the Vex widget on all platforms. This interface is more important as a
  * place to gather common Javadoc than as a way to enforce a contract.
  */
-public interface IVexWidget {
+public interface IVexWidget extends IDocumentEditor {
 
 	/*
 	 * Configuration
 	 */
 
 	/**
-	 * Returns the document associated with this component.
-	 */
-	IDocument getDocument();
-
-	/**
 	 * Sets a new document for this control.
 	 *
 	 * @param document
@@ -68,18 +51,9 @@
 	 */
 	void setStyleSheet(StyleSheet styleSheet);
 
-	/**
-	 * @return true if this widget is read-only
-	 */
-	boolean isReadOnly();
+	public void setWhitespacePolicy(IWhitespacePolicy whitespacePolicy);
 
-	/**
-	 * Make this widget read-only.
-	 *
-	 * @param readOnly
-	 *            set to true if this widget should be read-only
-	 */
-	void setReadOnly(boolean readOnly);
+	public IWhitespacePolicy getWhitespacePolicy();
 
 	/**
 	 * Returns the value of the debugging flag.
@@ -119,564 +93,4 @@
 	 */
 	ContentPosition viewToModel(int x, int y);
 
-	/*
-	 * Undo/Redo
-	 */
-
-	/**
-	 * Returns true if a redo can be performed.
-	 */
-	boolean canRedo();
-
-	/**
-	 * Redoes the last action on the redo stack.
-	 *
-	 * @throws CannotRedoException
-	 *             if the last action cannot be re-done, or if there is nothing to redo.
-	 */
-	void redo() throws CannotRedoException;
-
-	/**
-	 * Returns true if an undo can be performed.
-	 */
-	boolean canUndo();
-
-	/**
-	 * Undoes the last action on the undo stack.
-	 *
-	 * @throws CannotUndoException
-	 *             if the last action cannot be undone, or if there's nothing left to undo.
-	 */
-	void undo() throws CannotUndoException;
-
-	/*
-	 * Transaction Handling
-	 */
-
-	/**
-	 * Signals the start of a set of operations that should be considered a single unit for undo/redo purposes.
-	 *
-	 * <p>
-	 * <b>It is <i>strongly</i> recommended to use the {@link #doWork(IRunnable)} method instead of manually
-	 * implementing beginWork/endWork.</b>
-	 * </p>
-	 *
-	 * <p>
-	 * Each call to beginWork should be matched with a call to {@link #endWork(boolean)}. The following pattern can be
-	 * used to enforce this rules even in the face of exceptions.
-	 * </p>
-	 *
-	 * <pre>
-	 * VexComponent c = ...;
-	 * boolean success = false;
-	 * try {
-	 *     c.beginWork();
-	 *     // do multiple inserts/deletes
-	 *     success = true;
-	 * } finally {
-	 *     c.endWork(success);
-	 * }
-	 * </pre>
-	 *
-	 * <p>
-	 * In the case of nested beginWork/endWork calls, only the outermost results in an undoable event.
-	 * </p>
-	 *
-	 * @see endWork(boolean)
-	 */
-	void beginWork();
-
-	/**
-	 * Perform the runnable's run method within a beginWork/endWork pair. All operations in the runnable are treated as
-	 * a single unit of work, and can be undone in one operation by the user. Also, if a later operation fails, all
-	 * earlier operations are also undone.
-	 *
-	 * @param runnable
-	 *            Runnable implementing the work to be done.
-	 */
-	void doWork(Runnable runnable);
-
-	/**
-	 * Perform the runnable's run method within a beginWork/endWork pair. All operations in the runnable are treated as
-	 * a single unit of work, and can be undone in one operation by the user. Also, if a later operation fails, all
-	 * earlier operations are also undone.
-	 *
-	 * @param runnable
-	 *            Runnable implementing the work to be done.
-	 * @param savePosition
-	 *            If true, the current caret position is saved and restored once the operation is complete.
-	 */
-	void doWork(Runnable runnable, boolean savePosition);
-
-	/**
-	 * Signals the end of a set of operations that should be treated as a single unit for undo/redo purposes.
-	 *
-	 * @param success
-	 *            If true, an edit is added to the undo stack. If false, all the changes since the matching beginWork
-	 *            call are undone.
-	 *
-	 * @see #beginWork()
-	 */
-	void endWork(boolean success);
-
-	/**
-	 * Execute a Runnable, restoring the caret position to its original position afterward.
-	 *
-	 * @param runnable
-	 *            Runnable to be invoked.
-	 */
-	void savePosition(Runnable runnable);
-
-	/*
-	 * Clipboard cut/copy/paste
-	 */
-
-	/**
-	 * Cuts the current selection to the clipboard.
-	 */
-	void cutSelection();
-
-	/**
-	 * Copy the current selection to the clipboard.
-	 */
-	void copySelection();
-
-	/**
-	 * Returns true if the clipboard has content that can be pasted. Used to enable/disable the paste action of a
-	 * containing application.
-	 */
-	boolean canPaste();
-
-	/**
-	 * Paste the current clipboard contents into the document at the current caret position.
-	 */
-	void paste() throws DocumentValidationException;
-
-	/**
-	 * Returns true if the clipboard has plain text content that can be pasted. Used to enable/disable the "paste text"
-	 * action of a containing application.
-	 */
-	boolean canPasteText();
-
-	/**
-	 * Paste the current clipboard contents as plain text into the document at the current caret position.
-	 */
-	void pasteText() throws DocumentValidationException;
-
-	/*
-	 * Caret and Selection
-	 */
-
-	/**
-	 * Return the offset into the document represented by the caret.
-	 */
-	ContentPosition getCaretPosition();
-
-	/**
-	 * Returns the element at the current caret offset.
-	 */
-	IElement getCurrentElement();
-
-	/**
-	 * Returns the node a the current caret offset.
-	 */
-	INode getCurrentNode();
-
-	/**
-	 * Returns the offset range in the content which is selected.
-	 */
-	ContentRange getSelectedRange();
-
-	/**
-	 * Returns the {@link ContentPositionRange} which is selected.
-	 */
-	ContentPositionRange getSelectedPositionRange();
-
-	/**
-	 * Returns the currently selected document fragment, or null if there is no current selection.
-	 */
-	IDocumentFragment getSelectedFragment();
-
-	/**
-	 * Returns the currently selected string, or an empty string if there is no current selection.
-	 */
-	String getSelectedText();
-
-	/**
-	 * Returns true if the user currently has some text selected.
-	 */
-	boolean hasSelection();
-
-	/**
-	 * Selects all content in the document.
-	 */
-	void selectAll();
-
-	/**
-	 * Selects the word at the current caret offset.
-	 */
-	void selectWord();
-
-	/**
-	 * Selects the content of the given node.
-	 *
-	 * @param node
-	 *            the node
-	 */
-	void selectContentOf(INode node);
-
-	/**
-	 * Selects the given node.
-	 *
-	 * @param node
-	 *            the node to select
-	 */
-	void select(INode node);
-
-	/**
-	 * @return true if the current selection can be deleted. Returns false if there is no selection.
-	 */
-	boolean canDeleteSelection();
-
-	/**
-	 * Delete the current selection. Does nothing if there is no current selection.
-	 */
-	void deleteSelection();
-
-	/*
-	 * Caret Movement
-	 */
-
-	/**
-	 * Moves the caret a given distance relative to the current caret offset.
-	 *
-	 * @param distance
-	 *            Amount by which to alter the caret offset. Positive values increase the caret offset.
-	 */
-	void moveBy(int distance);
-
-	/**
-	 * Moves the caret a given distance relative to the current caret offset.
-	 *
-	 * @param distance
-	 *            Amount by which to alter the caret offset. Positive values increase the caret offset.
-	 * @param select
-	 *            if true, the current selection is extended to match the new caret offset
-	 */
-	void moveBy(int distance, boolean select);
-
-	/**
-	 * Moves the caret to a new offset. The selection is not extended. This is equivalent to
-	 * <code>moveTo(offset, false)</code>.
-	 *
-	 * @param int
-	 *            new offset for the caret. The offset must be >= 1 and less than the document size; if not, it is
-	 *            silently ignored.
-	 */
-	void moveTo(final ContentPosition position);
-
-	/**
-	 * Moves the caret to the new offset, possibly changing the selection.
-	 *
-	 * @param int
-	 *            new offset for the caret. The offset must be >= 1 and less than the document size; if not, it is
-	 *            silently ignored.
-	 * @param select
-	 *            if true, the current selection is extended to match the new caret offset.
-	 */
-	void moveTo(final ContentPosition position, boolean select);
-
-	/**
-	 * Move the caret to the end of the current line.
-	 *
-	 * @param select
-	 *            If true, the selection is extended.
-	 */
-	void moveToLineEnd(boolean select);
-
-	/**
-	 * Move the caret to the start of the current line.
-	 *
-	 * @param select
-	 *            If true, the selection is extended.
-	 */
-	void moveToLineStart(boolean select);
-
-	/**
-	 * Move the caret down to the next line. Attempts to preserve the same distance from the left edge of the control.
-	 *
-	 * @param select
-	 *            If true, the selection is extended.
-	 */
-	void moveToNextLine(boolean select);
-
-	/**
-	 * Move the caret down to the next page. Attempts to preserve the same distance from the left edge of the control.
-	 *
-	 * @param select
-	 *            If true, the selection is extended.
-	 */
-	void moveToNextPage(boolean select);
-
-	/**
-	 * Moves the caret to the end of the current or next word.
-	 *
-	 * @param select
-	 *            If true, the selection is extended.
-	 */
-	void moveToNextWord(boolean select);
-
-	/**
-	 * Moves the caret up to the previous line.
-	 *
-	 * @param select
-	 *            If true, the selection is extended
-	 */
-	void moveToPreviousLine(boolean select);
-
-	/**
-	 * Moves the caret up to the previous page.
-	 *
-	 * @param select
-	 *            If true, the selection is extended
-	 */
-	void moveToPreviousPage(boolean select);
-
-	/**
-	 * Moves the caret to the start of the current or previous word.
-	 *
-	 * @param select
-	 *            If true, the selection is extended.
-	 */
-	void moveToPreviousWord(boolean select);
-
-	/*
-	 * Namespaces
-	 */
-
-	void declareNamespace(final String namespacePrefix, final String namespaceURI);
-
-	void removeNamespace(final String namespacePrefix);
-
-	void declareDefaultNamespace(final String namespaceURI);
-
-	void removeDefaultNamespace();
-
-	/*
-	 * Attributes
-	 */
-
-	/**
-	 * @param attributeName
-	 *            local name of the attribute being changed.
-	 * @param value
-	 *            New value for the attribute. If null, the attribute is removed from the element.
-	 * @return true if the given value is valid for the attribute with the given name
-	 */
-	boolean canSetAttribute(String attributeName, String value);
-
-	/**
-	 * Sets the value of an attribute in the current element. Attributes set in this manner (as opposed to calling
-	 * Element.setAttribute directly) will be subject to undo/redo.
-	 *
-	 * @param attributeName
-	 *            local name of the attribute being changed.
-	 * @param value
-	 *            New value for the attribute. If null, the attribute is removed from the element.
-	 */
-	void setAttribute(String attributeName, String value);
-
-	/**
-	 * @param attributeName
-	 *            the local name of the attribute to remove
-	 * @return true if it is valid to remove the attribute with the given name
-	 */
-	boolean canRemoveAttribute(String attributeName);
-
-	/**
-	 * Removes an attribute from the current element. Attributes removed in this manner (as opposed to calling
-	 * Element.setAttribute directly) will be subject to undo/redo.
-	 *
-	 * @param attributeName
-	 *            local name of the attribute to remove.
-	 */
-	void removeAttribute(String attributeName);
-
-	/*
-	 * Content
-	 */
-
-	/**
-	 * Returns true if text can be inserted at the current position.
-	 */
-	boolean canInsertText();
-
-	/**
-	 * Inserts the given character at the current caret position. Any selected content is deleted. The main difference
-	 * between this method and insertText is that this method does not use beginWork/endWork, so consecutive calls to
-	 * insertChar are collapsed into a single IUndoableEdit. This method should normally only be called in response to a
-	 * user typing a key.
-	 *
-	 * @param c
-	 *            Character to insert.
-	 */
-	void insertChar(char c) throws DocumentValidationException;
-
-	/**
-	 * Deletes the character to the right of the caret.
-	 */
-	void deleteNextChar() throws DocumentValidationException;
-
-	/**
-	 * Deletes the character to the left of the caret.
-	 */
-	void deletePreviousChar() throws DocumentValidationException;
-
-	/**
-	 * Inserts the given text at the current caret position. Any selected content is first deleted.
-	 *
-	 * @param text
-	 *            String to insert.
-	 */
-	void insertText(String text) throws DocumentValidationException;
-
-	/*
-	 * Structure
-	 */
-
-	/**
-	 * Returns an array of names of elements that are valid to insert at the given caret offset and selection
-	 */
-	ElementName[] getValidInsertElements();
-
-	/**
-	 * Returns an array of names of elements to which the element at the current caret location can be morphed.
-	 */
-	ElementName[] getValidMorphElements();
-
-	/**
-	 * @param elementName
-	 *            the qualified name of the element to insert
-	 * @return true if an element with the given name can be inserted at the current caret position/instead of the
-	 *         current selection
-	 */
-	boolean canInsertElement(QualifiedName elementName);
-
-	/**
-	 * Inserts the given element at the current caret position. Any selected content becomes the new contents of the
-	 * element.
-	 *
-	 * @param elementName
-	 *            Qualified name of the element to insert.
-	 * @return the newly inserted element
-	 */
-	IElement insertElement(QualifiedName elementName) throws DocumentValidationException;
-
-	/**
-	 * @return true if a comment can be inserted at the current caret position/instead of the current selection
-	 */
-	boolean canInsertComment();
-
-	/**
-	 * Inserts a comment a the current caret position. Any selected content is first deleted.
-	 *
-	 * @return the new comment
-	 */
-	IComment insertComment() throws DocumentValidationException;
-
-	/**
-	 * @return true if a processing instruction can be inserted at the current caret position/instead of the current
-	 *         selection
-	 */
-	boolean canInsertProcessingInstruction();
-
-	/**
-	 * Inserts a processing instruction at the current caret position. Any selected content is first deleted.
-	 *
-	 * @return the new comment
-	 */
-	IProcessingInstruction insertProcessingInstruction(final String target) throws CannotRedoException, ReadOnlyException;
-
-	/**
-	 * Edits the processing instruction at the current caret position. Updates target and data with the given Strings.
-	 *
-	 * @param target
-	 *            The target to set. may be null to keep the old target.
-	 * @param data
-	 *            The data to set. May be null to keep the old value.
-	 */
-	void editProcessingInstruction(final String target, final String data) throws CannotRedoException, ReadOnlyException;
-
-	/**
-	 * Inserts the given XML fragment at the current caret position. Any selected content is first deleted.
-	 *
-	 * @param xml
-	 *            XML to insert
-	 * @throws DocumentValidationException
-	 */
-	public void insertXML(String xml) throws DocumentValidationException;
-
-	/**
-	 * Returns true if the given fragment can be inserted at the current caret position.
-	 *
-	 * @param fragment
-	 *            DocumentFragment to be inserted.
-	 */
-	boolean canInsertFragment(final IDocumentFragment fragment);
-
-	/**
-	 * Inserts the given document fragment at the current caret position. Any selected content is deleted.
-	 *
-	 * @param frag
-	 *            DocumentFragment to insert.
-	 */
-	void insertFragment(IDocumentFragment frag) throws DocumentValidationException;
-
-	/**
-	 * Returns true if the current element can be unwrapped, i.e. replaced with its content.
-	 */
-	boolean canUnwrap();
-
-	void unwrap() throws DocumentValidationException;
-
-	/**
-	 * Indicates whether the current element can be morphed into the given element.
-	 *
-	 * @param elementName
-	 *            Qualified name of the element to morph the current element into.
-	 * @return true if the current element can be morphed
-	 */
-	boolean canMorph(QualifiedName elementName);
-
-	/**
-	 * Replaces the current element with an element with the given name. The content of the element is preserved.
-	 *
-	 * @param elementName
-	 *            Qualified name of the element to replace the current element with.
-	 * @throws DocumentValidationException
-	 *             if the given element is not valid at this place in the document, or if the current element's content
-	 *             is not compatible with the given element.
-	 */
-	void morph(QualifiedName elementName) throws DocumentValidationException;
-
-	boolean canJoin();
-
-	void join() throws DocumentValidationException;
-
-	public void setWhitespacePolicy(IWhitespacePolicy whitespacePolicy);
-
-	public IWhitespacePolicy getWhitespacePolicy();
-
-	/**
-	 * Indicates whether the current element can be splitted into two elements at the current caret position.
-	 *
-	 * @return true if the current element can be splitted
-	 */
-	boolean canSplit();
-
-	/**
-	 * Split the element at the current caret offset. This is the normal behaviour when the user presses Enter.
-	 */
-	void split() throws DocumentValidationException;
-
 }
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/IViewPort.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/IViewPort.java
new file mode 100644
index 0000000..283ad74
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/IViewPort.java
@@ -0,0 +1,25 @@
+/*******************************************************************************
+ * 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.widget;
+
+import org.eclipse.vex.core.internal.core.Rectangle;
+
+/**
+ * @author Florian Thienel
+ */
+public interface IViewPort {
+
+	void reconcile(int maximumHeight);
+
+	Rectangle getVisibleArea();
+
+	void moveRelative(int delta);
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/InMemoryClipboard.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/InMemoryClipboard.java
new file mode 100644
index 0000000..016f5bc
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/InMemoryClipboard.java
@@ -0,0 +1,78 @@
+/*******************************************************************************
+ * 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.provisional.dom.DocumentValidationException;
+import org.eclipse.vex.core.provisional.dom.IDocumentFragment;
+
+public class InMemoryClipboard implements IClipboard {
+
+	private IDocumentFragment content;
+
+	@Override
+	public void dispose() {
+		content = null;
+	}
+
+	@Override
+	public void cutSelection(final IDocumentEditor editor) {
+		copySelection(editor);
+		editor.deleteSelection();
+	}
+
+	@Override
+	public void copySelection(final IDocumentEditor editor) {
+		if (!editor.hasSelection()) {
+			return;
+		}
+
+		content = editor.getSelectedFragment();
+	}
+
+	@Override
+	public boolean hasContent() {
+		if (content == null) {
+			return false;
+		}
+		return true;
+	}
+
+	@Override
+	public void paste(final IDocumentEditor editor) throws DocumentValidationException {
+		if (!hasContent()) {
+			return;
+		}
+
+		editor.insertFragment(content);
+	}
+
+	@Override
+	public boolean hasTextContent() {
+		if (content == null) {
+			return false;
+		}
+		final String text = content.getText();
+		if (text == null || "".equals(text)) {
+			return false;
+		}
+		return true;
+	}
+
+	@Override
+	public void pasteText(final IDocumentEditor editor) throws DocumentValidationException {
+		if (!hasTextContent()) {
+			return;
+		}
+
+		editor.insertText(content.getText());
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/SimpleSelector.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/SimpleSelector.java
new file mode 100644
index 0000000..a606902
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/SimpleSelector.java
@@ -0,0 +1,49 @@
+/*******************************************************************************
+ * 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.widget;
+
+/**
+ * @author Florian Thienel
+ */
+public class SimpleSelector extends BaseSelector {
+
+	@Override
+	public void moveEndTo(final int offset) {
+		setEndAbsoluteTo(offset);
+	}
+
+	@Override
+	public void setEndAbsoluteTo(final int offset) {
+		final boolean movingForward = offset > getCaretOffset();
+		final boolean movingBackward = offset < getCaretOffset();
+		final boolean movingTowardMark = movingForward && getMark() >= offset || movingBackward && getMark() <= offset;
+		final boolean movingAwayFromMark = !movingTowardMark;
+
+		if (movingForward && movingTowardMark) {
+			setStartOffset(offset);
+			setEndOffset(getMark());
+			setCaretOffset(getStartOffset());
+		} else if (movingBackward && movingTowardMark) {
+			setStartOffset(getMark());
+			setEndOffset(offset);
+			setCaretOffset(getEndOffset());
+		} else if (movingForward && movingAwayFromMark) {
+			setStartOffset(getMark());
+			setEndOffset(offset);
+			setCaretOffset(getEndOffset());
+		} else if (movingBackward && movingAwayFromMark) {
+			setStartOffset(offset);
+			setEndOffset(getMark());
+			setCaretOffset(getStartOffset());
+		}
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/VisualizationController.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/VisualizationController.java
new file mode 100644
index 0000000..f1cfa4c
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/VisualizationController.java
@@ -0,0 +1,126 @@
+/*******************************************************************************
+ * 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.widget;
+
+import org.eclipse.vex.core.internal.cursor.Cursor;
+import org.eclipse.vex.core.internal.cursor.ICursorPositionListener;
+import org.eclipse.vex.core.internal.visualization.IBoxModelBuilder;
+import org.eclipse.vex.core.provisional.dom.AttributeChangeEvent;
+import org.eclipse.vex.core.provisional.dom.ContentChangeEvent;
+import org.eclipse.vex.core.provisional.dom.IDocument;
+import org.eclipse.vex.core.provisional.dom.IDocumentListener;
+import org.eclipse.vex.core.provisional.dom.NamespaceDeclarationChangeEvent;
+
+/**
+ * @author Florian Thienel
+ */
+public class VisualizationController {
+
+	private IDocument document;
+	private final BoxView view;
+	private final DOMVisualization visualization;
+
+	private final IDocumentListener documentListener = new IDocumentListener() {
+		@Override
+		public void attributeChanged(final AttributeChangeEvent event) {
+			visualization.rebuildContentRange(event.getParent(), event.getParent().getRange());
+		}
+
+		@Override
+		public void namespaceChanged(final NamespaceDeclarationChangeEvent event) {
+		}
+
+		@Override
+		public void beforeContentDeleted(final ContentChangeEvent event) {
+		}
+
+		@Override
+		public void beforeContentInserted(final ContentChangeEvent event) {
+		}
+
+		@Override
+		public void contentDeleted(final ContentChangeEvent event) {
+			if (event.isStructuralChange()) {
+				visualization.rebuildStructure(event.getParent());
+			} else {
+				visualization.rebuildContentRange(event.getParent(), event.getRange());
+			}
+		}
+
+		@Override
+		public void contentInserted(final ContentChangeEvent event) {
+			if (event.isStructuralChange()) {
+				visualization.rebuildStructure(event.getParent());
+			} else {
+				visualization.rebuildContentRange(event.getParent(), event.getRange());
+			}
+		}
+	};
+
+	private final ICursorPositionListener cursorListener = new ICursorPositionListener() {
+		@Override
+		public void positionAboutToChange() {
+			view.invalidateCursor();
+		}
+
+		@Override
+		public void positionChanged(final int offset) {
+			// ignore
+		}
+	};
+
+	public VisualizationController(final IRenderer renderer, final IViewPort viewPort, final Cursor cursor) {
+		cursor.addPositionListener(cursorListener);
+		view = new BoxView(renderer, viewPort, cursor);
+		visualization = new DOMVisualization(cursor, view);
+	}
+
+	public void dispose() {
+		view.dispose();
+	}
+
+	public void setDocument(final IDocument document) {
+		disconnectDocumentListener();
+		this.document = document;
+		connectDocumentListener();
+
+		visualization.setDocument(document);
+	}
+
+	private void connectDocumentListener() {
+		if (document != null) {
+			document.addDocumentListener(documentListener);
+		}
+	}
+
+	private void disconnectDocumentListener() {
+		if (document != null) {
+			document.removeDocumentListener(documentListener);
+		}
+	}
+
+	public void setBoxModelBuilder(final IBoxModelBuilder boxModelBuilder) {
+		visualization.setBoxModelBuilder(boxModelBuilder);
+	}
+
+	public void refreshAll() {
+		visualization.buildAll();
+		view.invalidateEverything();
+	}
+
+	public void refreshViewport() {
+		view.invalidateViewport();
+	}
+
+	public void resize(final int width) {
+		view.invalidateWidth(width);
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/BoxWidget.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/BoxWidget.java
new file mode 100644
index 0000000..5d57144
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/BoxWidget.java
@@ -0,0 +1,899 @@
+/*******************************************************************************
+ * Copyright (c) 2014, 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 static org.eclipse.vex.core.internal.cursor.CursorMoves.toAbsoluteCoordinates;
+import static org.eclipse.vex.core.internal.cursor.CursorMoves.toOffset;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.core.runtime.Assert;
+import org.eclipse.core.runtime.ListenerList;
+import org.eclipse.core.runtime.QualifiedName;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.ISelectionProvider;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ControlAdapter;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.KeyAdapter;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.MouseAdapter;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseMoveListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.widgets.Canvas;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.vex.core.internal.core.ElementName;
+import org.eclipse.vex.core.internal.core.Rectangle;
+import org.eclipse.vex.core.internal.css.IWhitespacePolicy;
+import org.eclipse.vex.core.internal.cursor.Cursor;
+import org.eclipse.vex.core.internal.cursor.ICursorMove;
+import org.eclipse.vex.core.internal.cursor.ICursorPositionListener;
+import org.eclipse.vex.core.internal.undo.CannotApplyException;
+import org.eclipse.vex.core.internal.undo.CannotUndoException;
+import org.eclipse.vex.core.internal.visualization.IBoxModelBuilder;
+import org.eclipse.vex.core.internal.widget.BalancingSelector;
+import org.eclipse.vex.core.internal.widget.DocumentEditor;
+import org.eclipse.vex.core.internal.widget.IDocumentEditor;
+import org.eclipse.vex.core.internal.widget.ITableModel;
+import org.eclipse.vex.core.internal.widget.IViewPort;
+import org.eclipse.vex.core.internal.widget.ReadOnlyException;
+import org.eclipse.vex.core.internal.widget.VisualizationController;
+import org.eclipse.vex.core.provisional.dom.ContentPosition;
+import org.eclipse.vex.core.provisional.dom.ContentPositionRange;
+import org.eclipse.vex.core.provisional.dom.ContentRange;
+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.IProcessingInstruction;
+
+/**
+ * A widget to display the new box model.
+ *
+ * @author Florian Thienel
+ */
+public class BoxWidget extends Canvas implements ISelectionProvider, IDocumentEditor {
+
+	private static final int SELECTION_CHANGE_NOTIFICATION_DELAY = 100;
+	private static final char CHAR_NONE = 0;
+	private static final Map<KeyStroke, IVexWidgetHandler> KEY_MAP = buildKeyMap();
+
+	private IDocument document;
+
+	private final org.eclipse.swt.graphics.Cursor mouseCursor;
+
+	private final Cursor cursor;
+	private final BalancingSelector selector;
+	private final VisualizationController controller;
+	private final SwtClipboard clipboard;
+	private final DocumentEditor editor;
+
+	private final ListenerList selectionChangedListeners = new ListenerList();
+	private Runnable lastSelectionChangeNotification = null;
+
+	public BoxWidget(final Composite parent, final int style) {
+		super(parent, style | SWT.NO_BACKGROUND);
+
+		mouseCursor = new org.eclipse.swt.graphics.Cursor(parent.getDisplay(), SWT.CURSOR_IBEAM);
+		setCursor(mouseCursor);
+
+		connectDispose();
+		connectResize();
+		if ((style & SWT.V_SCROLL) == SWT.V_SCROLL) {
+			connectScrollVertically();
+		}
+		connectKeyboard();
+		connectMouse();
+
+		final ViewPort viewPort = new ViewPort();
+
+		selector = new BalancingSelector();
+		cursor = new Cursor(selector, viewPort);
+		connectCursor();
+
+		controller = new VisualizationController(new DoubleBufferedRenderer(this), viewPort, cursor);
+		clipboard = new SwtClipboard(parent.getDisplay());
+		editor = new DocumentEditor(cursor, IWhitespacePolicy.NULL, clipboard);
+	}
+
+	public void setDocument(final IDocument document) {
+		this.document = document;
+		selector.setDocument(document);
+		controller.setDocument(document);
+		editor.setDocument(document);
+	}
+
+	public IDocument getDocument() {
+		return document;
+	}
+
+	public void setBoxModelBuilder(final IBoxModelBuilder boxModelBuilder) {
+		controller.setBoxModelBuilder(boxModelBuilder);
+	}
+
+	private void connectDispose() {
+		addDisposeListener(new DisposeListener() {
+			@Override
+			public void widgetDisposed(final DisposeEvent e) {
+				BoxWidget.this.widgetDisposed();
+			}
+		});
+	}
+
+	private void connectResize() {
+		addControlListener(new ControlAdapter() {
+			@Override
+			public void controlResized(final ControlEvent e) {
+				resize(e);
+			}
+		});
+	}
+
+	private void connectScrollVertically() {
+		getVerticalBar().addSelectionListener(new SelectionAdapter() {
+			@Override
+			public void widgetSelected(final SelectionEvent e) {
+				scrollVertically(e);
+			}
+		});
+	}
+
+	private void connectKeyboard() {
+		addKeyListener(new KeyAdapter() {
+			@Override
+			public void keyPressed(final KeyEvent e) {
+				BoxWidget.this.keyPressed(e);
+			}
+		});
+	}
+
+	private void connectMouse() {
+		addMouseListener(new MouseAdapter() {
+			@Override
+			public void mouseDoubleClick(final MouseEvent e) {
+				BoxWidget.this.mouseDoubleClick(e);
+			}
+
+			@Override
+			public void mouseDown(final MouseEvent e) {
+				BoxWidget.this.mouseDown(e);
+			}
+		});
+		addMouseMoveListener(new MouseMoveListener() {
+			@Override
+			public void mouseMove(final MouseEvent e) {
+				BoxWidget.this.mouseMove(e);
+			}
+		});
+	}
+
+	private void connectCursor() {
+		cursor.addPositionListener(new ICursorPositionListener() {
+			@Override
+			public void positionAboutToChange() {
+				// ignore
+			}
+
+			@Override
+			public void positionChanged(final int offset) {
+				cursorPositionChanged(offset);
+			}
+		});
+	}
+
+	private void widgetDisposed() {
+		controller.dispose();
+		mouseCursor.dispose();
+		clipboard.dispose();
+	}
+
+	private void resize(final ControlEvent event) {
+		controller.resize(getClientArea().width);
+	}
+
+	private void scrollVertically(final SelectionEvent event) {
+		controller.refreshViewport();
+	}
+
+	private void keyPressed(final KeyEvent event) {
+		final KeyStroke keyStroke = new KeyStroke(event);
+		final IVexWidgetHandler handler = KEY_MAP.get(keyStroke);
+		if (handler != null) {
+			try {
+				handler.execute(new ExecutionEvent(null, Collections.emptyMap(), event, null), BoxWidget.this);
+			} catch (final ReadOnlyException e) {
+				// TODO give feedback: the editor is read-only
+			} catch (final Exception ex) {
+				ex.printStackTrace();
+			}
+		} else if (!Character.isISOControl(event.character)) {
+			try {
+				insertChar(event.character);
+			} catch (final DocumentValidationException e) {
+				// TODO give feedback: at this document position no character can be entered
+			} catch (final ReadOnlyException e) {
+				// TODO give feedback: the editor is read-only
+			}
+		}
+	}
+
+	private void moveOrSelect(final int stateMask, final ICursorMove move) {
+		if ((stateMask & SWT.SHIFT) == SWT.SHIFT) {
+			cursor.select(move);
+		} else {
+			cursor.move(move);
+		}
+	}
+
+	private void mouseDoubleClick(final MouseEvent event) {
+		if (event.button == 1) {
+			selectWord();
+		}
+	}
+
+	private void mouseDown(final MouseEvent event) {
+		if (event.button == 1) {
+			final int absoluteY = event.y + getVerticalBar().getSelection();
+			moveOrSelect(event.stateMask, toAbsoluteCoordinates(event.x, absoluteY));
+		}
+	}
+
+	private void mouseMove(final MouseEvent event) {
+		if ((event.stateMask & SWT.BUTTON_MASK) != SWT.BUTTON1) {
+			return;
+		}
+		final int absoluteY = event.y + getVerticalBar().getSelection();
+		cursor.select(toAbsoluteCoordinates(event.x, absoluteY));
+	}
+
+	private void cursorPositionChanged(final int offset) {
+		fireSelectionChanged(new VexSelection(editor));
+	}
+
+	public void refresh() {
+		controller.refreshAll();
+	}
+
+	public Rectangle getCaretArea() {
+		return cursor.getCaretArea();
+	}
+
+	/*
+	 * ISelectionProvider
+	 */
+
+	@Override
+	public IVexSelection getSelection() {
+		return new VexSelection(editor);
+	}
+
+	@Override
+	public void setSelection(final ISelection selection) {
+		Assert.isLegal(selection instanceof IVexSelection, "BoxWidget can only handle instances of IVexSelection");
+		final IVexSelection vexSelection = (IVexSelection) selection;
+
+		if (vexSelection.isEmpty()) {
+			cursor.move(toOffset(vexSelection.getCaretOffset()));
+		} else {
+			final ContentRange selectedRange = vexSelection.getSelectedRange();
+			cursor.move(toOffset(selectedRange.getStartOffset()));
+			cursor.select(toOffset(selectedRange.getEndOffset()));
+		}
+	};
+
+	@Override
+	public void addSelectionChangedListener(final ISelectionChangedListener listener) {
+		selectionChangedListeners.add(listener);
+	}
+
+	@Override
+	public void removeSelectionChangedListener(final ISelectionChangedListener listener) {
+		selectionChangedListeners.remove(listener);
+	}
+
+	private void fireSelectionChanged(final IVexSelection selection) {
+		lastSelectionChangeNotification = new Runnable() {
+			@Override
+			public void run() {
+				if (lastSelectionChangeNotification != this) {
+					return;
+				}
+
+				for (final Object listener : selectionChangedListeners.getListeners()) {
+					try {
+						((ISelectionChangedListener) listener).selectionChanged(new SelectionChangedEvent(BoxWidget.this, selection));
+					} catch (final Throwable t) {
+						t.printStackTrace();
+						// TODO remove listener?
+					}
+				}
+			}
+		};
+		getDisplay().timerExec(SELECTION_CHANGE_NOTIFICATION_DELAY, lastSelectionChangeNotification);
+	}
+
+	/*
+	 * IDocumentEditor
+	 */
+
+	@Override
+	public IWhitespacePolicy getWhitespacePolicy() {
+		return editor.getWhitespacePolicy();
+	}
+
+	@Override
+	public void setWhitespacePolicy(final IWhitespacePolicy policy) {
+		editor.setWhitespacePolicy(policy);
+	}
+
+	@Override
+	public ITableModel getTableModel() {
+		return editor.getTableModel();
+	}
+
+	@Override
+	public void setTableModel(final ITableModel tableModel) {
+		editor.setTableModel(tableModel);
+	}
+
+	public boolean isReadOnly() {
+		return editor.isReadOnly();
+	}
+
+	public void setReadOnly(final boolean readOnly) {
+		editor.setReadOnly(readOnly);
+	}
+
+	public boolean canRedo() {
+		return editor.canRedo();
+	}
+
+	public void redo() throws CannotApplyException {
+		editor.redo();
+	}
+
+	public boolean canUndo() {
+		return editor.canUndo();
+	}
+
+	public void undo() throws CannotUndoException {
+		editor.undo();
+	}
+
+	public boolean isDirty() {
+		return editor.isDirty();
+	}
+
+	public void markClean() {
+		editor.markClean();
+	}
+
+	public void doWork(final Runnable runnable) throws CannotApplyException {
+		editor.doWork(runnable);
+	}
+
+	public void doWork(final Runnable runnable, final boolean savePosition) throws CannotApplyException {
+		editor.doWork(runnable, savePosition);
+	}
+
+	public void savePosition(final Runnable runnable) {
+		editor.savePosition(runnable);
+	}
+
+	public void cutSelection() {
+		editor.cutSelection();
+	}
+
+	public void copySelection() {
+		editor.copySelection();
+	}
+
+	public boolean canPaste() {
+		return editor.canPaste();
+	}
+
+	public void paste() throws DocumentValidationException {
+		editor.paste();
+	}
+
+	public boolean canPasteText() {
+		return editor.canPasteText();
+	}
+
+	public void pasteText() throws DocumentValidationException {
+		editor.pasteText();
+	}
+
+	public ContentPosition getCaretPosition() {
+		return editor.getCaretPosition();
+	}
+
+	public IElement getCurrentElement() {
+		return editor.getCurrentElement();
+	}
+
+	public INode getCurrentNode() {
+		return editor.getCurrentNode();
+	}
+
+	@Override
+	public String toString() {
+		return editor.toString();
+	}
+
+	public boolean hasSelection() {
+		return editor.hasSelection();
+	}
+
+	public ContentRange getSelectedRange() {
+		return editor.getSelectedRange();
+	}
+
+	public ContentPositionRange getSelectedPositionRange() {
+		return editor.getSelectedPositionRange();
+	}
+
+	public IDocumentFragment getSelectedFragment() {
+		return editor.getSelectedFragment();
+	}
+
+	public String getSelectedText() {
+		return editor.getSelectedText();
+	}
+
+	public void selectAll() {
+		editor.selectAll();
+	}
+
+	public void selectWord() {
+		editor.selectWord();
+	}
+
+	public void selectContentOf(final INode node) {
+		editor.selectContentOf(node);
+	}
+
+	public void select(final INode node) {
+		editor.select(node);
+	}
+
+	public boolean canDeleteSelection() {
+		return editor.canDeleteSelection();
+	}
+
+	public void deleteSelection() {
+		editor.deleteSelection();
+	}
+
+	public void moveBy(final int distance) {
+		editor.moveBy(distance);
+	}
+
+	public void moveBy(final int distance, final boolean select) {
+		editor.moveBy(distance, select);
+	}
+
+	public void moveTo(final ContentPosition position) {
+		editor.moveTo(position);
+	}
+
+	public void moveTo(final ContentPosition position, final boolean select) {
+		editor.moveTo(position, select);
+	}
+
+	public void moveToLineEnd(final boolean select) {
+		editor.moveToLineEnd(select);
+	}
+
+	public void moveToLineStart(final boolean select) {
+		editor.moveToLineStart(select);
+	}
+
+	public void moveToNextLine(final boolean select) {
+		editor.moveToNextLine(select);
+	}
+
+	public void moveToNextPage(final boolean select) {
+		editor.moveToNextPage(select);
+	}
+
+	public void moveToNextWord(final boolean select) {
+		editor.moveToNextWord(select);
+	}
+
+	public void moveToPreviousLine(final boolean select) {
+		editor.moveToPreviousLine(select);
+	}
+
+	public void moveToPreviousPage(final boolean select) {
+		editor.moveToPreviousPage(select);
+	}
+
+	public void moveToPreviousWord(final boolean select) {
+		editor.moveToPreviousWord(select);
+	}
+
+	public void declareNamespace(final String namespacePrefix, final String namespaceURI) {
+		editor.declareNamespace(namespacePrefix, namespaceURI);
+	}
+
+	public void removeNamespace(final String namespacePrefix) {
+		editor.removeNamespace(namespacePrefix);
+	}
+
+	public void declareDefaultNamespace(final String namespaceURI) {
+		editor.declareDefaultNamespace(namespaceURI);
+	}
+
+	public void removeDefaultNamespace() {
+		editor.removeDefaultNamespace();
+	}
+
+	public boolean canSetAttribute(final String attributeName, final String value) {
+		return editor.canSetAttribute(attributeName, value);
+	}
+
+	public void setAttribute(final String attributeName, final String value) {
+		editor.setAttribute(attributeName, value);
+	}
+
+	public boolean canRemoveAttribute(final String attributeName) {
+		return editor.canRemoveAttribute(attributeName);
+	}
+
+	public void removeAttribute(final String attributeName) {
+		editor.removeAttribute(attributeName);
+	}
+
+	public boolean canInsertText() {
+		return editor.canInsertText();
+	}
+
+	public void insertChar(final char c) throws DocumentValidationException {
+		editor.insertChar(c);
+	}
+
+	@Override
+	public void insertLineBreak() throws DocumentValidationException {
+		editor.insertLineBreak();
+	}
+
+	public void deleteForward() throws DocumentValidationException {
+		editor.deleteForward();
+	}
+
+	public void deleteBackward() throws DocumentValidationException {
+		editor.deleteBackward();
+	}
+
+	public void insertText(final String text) throws DocumentValidationException {
+		editor.insertText(text);
+	}
+
+	public void insertXML(final String xml) throws DocumentValidationException {
+		editor.insertXML(xml);
+	}
+
+	public ElementName[] getValidInsertElements() {
+		return editor.getValidInsertElements();
+	}
+
+	public ElementName[] getValidMorphElements() {
+		return editor.getValidMorphElements();
+	}
+
+	public boolean canInsertElement(final QualifiedName elementName) {
+		return editor.canInsertElement(elementName);
+	}
+
+	public IElement insertElement(final QualifiedName elementName) throws DocumentValidationException {
+		return editor.insertElement(elementName);
+	}
+
+	public boolean canInsertComment() {
+		return editor.canInsertComment();
+	}
+
+	public IComment insertComment() throws DocumentValidationException {
+		return editor.insertComment();
+	}
+
+	public boolean canInsertProcessingInstruction() {
+		return editor.canInsertProcessingInstruction();
+	}
+
+	public IProcessingInstruction insertProcessingInstruction(final String target) throws CannotApplyException, ReadOnlyException {
+		return editor.insertProcessingInstruction(target);
+	}
+
+	public void editProcessingInstruction(final String target, final String data) throws CannotApplyException, ReadOnlyException {
+		editor.editProcessingInstruction(target, data);
+	}
+
+	public boolean canInsertFragment(final IDocumentFragment fragment) {
+		return editor.canInsertFragment(fragment);
+	}
+
+	public void insertFragment(final IDocumentFragment fragment) throws DocumentValidationException {
+		editor.insertFragment(fragment);
+	}
+
+	public boolean canUnwrap() {
+		return editor.canUnwrap();
+	}
+
+	public void unwrap() throws DocumentValidationException {
+		editor.unwrap();
+	}
+
+	public boolean canMorph(final QualifiedName elementName) {
+		return editor.canMorph(elementName);
+	}
+
+	public void morph(final QualifiedName elementName) throws DocumentValidationException {
+		editor.morph(elementName);
+	}
+
+	public boolean canJoin() {
+		return editor.canJoin();
+	}
+
+	public void join() throws DocumentValidationException {
+		editor.join();
+	}
+
+	public boolean canSplit() {
+		return editor.canSplit();
+	}
+
+	public void split() throws DocumentValidationException {
+		editor.split();
+	}
+
+	/*
+	 * Key Map
+	 */
+
+	private static void addKey(final Map<KeyStroke, IVexWidgetHandler> keyMap, final char character, final int keyCode, final int stateMask, final IVexWidgetHandler action) {
+		keyMap.put(new KeyStroke(character, keyCode, stateMask), action);
+	}
+
+	private static Map<KeyStroke, IVexWidgetHandler> buildKeyMap() {
+		final Map<KeyStroke, IVexWidgetHandler> keyMap = new HashMap<KeyStroke, IVexWidgetHandler>();
+
+		// arrows: (Shift) Up/Down, {-, Shift, Ctrl, Shift+Ctrl} + Left/Right
+		addKey(keyMap, CHAR_NONE, SWT.ARROW_DOWN, SWT.NONE, new IVexWidgetHandler() {
+			@Override
+			public void execute(final ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+				editor.moveToNextLine(false);
+			}
+		});
+		addKey(keyMap, CHAR_NONE, SWT.ARROW_DOWN, SWT.SHIFT, new IVexWidgetHandler() {
+			@Override
+			public void execute(final ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+				editor.moveToNextLine(true);
+			}
+		});
+		addKey(keyMap, CHAR_NONE, SWT.ARROW_UP, SWT.NONE, new IVexWidgetHandler() {
+			@Override
+			public void execute(final ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+				editor.moveToPreviousLine(false);
+			}
+		});
+		addKey(keyMap, CHAR_NONE, SWT.ARROW_UP, SWT.SHIFT, new IVexWidgetHandler() {
+			@Override
+			public void execute(final ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+				editor.moveToPreviousLine(true);
+			}
+		});
+		addKey(keyMap, CHAR_NONE, SWT.ARROW_LEFT, SWT.NONE, new IVexWidgetHandler() {
+			@Override
+			public void execute(final ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+				editor.moveBy(-1);
+			}
+		});
+		addKey(keyMap, CHAR_NONE, SWT.ARROW_LEFT, SWT.SHIFT, new IVexWidgetHandler() {
+			@Override
+			public void execute(final ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+				editor.moveBy(-1, true);
+			}
+		});
+		addKey(keyMap, CHAR_NONE, SWT.ARROW_LEFT, SWT.CONTROL, new IVexWidgetHandler() {
+			@Override
+			public void execute(final ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+				editor.moveToPreviousWord(false);
+			}
+		});
+		addKey(keyMap, CHAR_NONE, SWT.ARROW_LEFT, SWT.SHIFT | SWT.CONTROL, new IVexWidgetHandler() {
+			@Override
+			public void execute(final ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+				editor.moveToPreviousWord(true);
+			}
+		});
+		addKey(keyMap, CHAR_NONE, SWT.ARROW_RIGHT, SWT.NONE, new IVexWidgetHandler() {
+			@Override
+			public void execute(final ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+				editor.moveBy(+1);
+			}
+		});
+		addKey(keyMap, CHAR_NONE, SWT.ARROW_RIGHT, SWT.SHIFT, new IVexWidgetHandler() {
+			@Override
+			public void execute(final ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+				editor.moveBy(+1, true);
+			}
+		});
+		addKey(keyMap, CHAR_NONE, SWT.ARROW_RIGHT, SWT.CONTROL, new IVexWidgetHandler() {
+			@Override
+			public void execute(final ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+				editor.moveToNextWord(false);
+			}
+		});
+		addKey(keyMap, CHAR_NONE, SWT.ARROW_RIGHT, SWT.SHIFT | SWT.CONTROL, new IVexWidgetHandler() {
+			@Override
+			public void execute(final ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+				editor.moveToNextWord(true);
+			}
+		});
+
+		// Delete/Backspace
+		addKey(keyMap, SWT.BS, SWT.BS, SWT.NONE, new IVexWidgetHandler() {
+			@Override
+			public void execute(final ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+				try {
+					editor.deleteBackward();
+				} catch (final DocumentValidationException e) {
+					throw new ExecutionException(e.getMessage(), e);
+				}
+			}
+		});
+		addKey(keyMap, SWT.DEL, SWT.DEL, SWT.NONE, new IVexWidgetHandler() {
+			@Override
+			public void execute(final ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+				try {
+					editor.deleteForward();
+				} catch (final DocumentValidationException e) {
+					throw new ExecutionException(e.getMessage(), e);
+				}
+			}
+		});
+
+		// Tab
+		addKey(keyMap, SWT.TAB, SWT.TAB, SWT.NONE, new IVexWidgetHandler() {
+			@Override
+			public void execute(final ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+				try {
+					editor.insertChar('\t');
+				} catch (final DocumentValidationException e) {
+					throw new ExecutionException(e.getMessage(), e);
+				}
+			}
+		});
+
+		// {-, Shift, Ctrl, Shift+Ctrl} + Home/End
+		addKey(keyMap, CHAR_NONE, SWT.END, SWT.NONE, new IVexWidgetHandler() {
+			@Override
+			public void execute(final ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+				editor.moveToLineEnd(false);
+			}
+		});
+		addKey(keyMap, CHAR_NONE, SWT.END, SWT.SHIFT, new IVexWidgetHandler() {
+			@Override
+			public void execute(final ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+				editor.moveToLineEnd(true);
+			}
+		});
+		addKey(keyMap, CHAR_NONE, SWT.END, SWT.CONTROL, new IVexWidgetHandler() {
+			@Override
+			public void execute(final ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+				editor.moveTo(editor.getDocument().getEndPosition().moveBy(-1));
+			}
+		});
+		addKey(keyMap, CHAR_NONE, SWT.END, SWT.SHIFT | SWT.CONTROL, new IVexWidgetHandler() {
+			@Override
+			public void execute(final ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+				editor.moveTo(editor.getDocument().getEndPosition().moveBy(-1));
+			}
+		});
+		addKey(keyMap, CHAR_NONE, SWT.HOME, SWT.NONE, new IVexWidgetHandler() {
+			@Override
+			public void execute(final ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+				editor.moveToLineStart(false);
+			}
+		});
+		addKey(keyMap, CHAR_NONE, SWT.HOME, SWT.SHIFT, new IVexWidgetHandler() {
+			@Override
+			public void execute(final ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+				editor.moveToLineStart(true);
+			}
+		});
+		addKey(keyMap, CHAR_NONE, SWT.HOME, SWT.CONTROL, new IVexWidgetHandler() {
+			@Override
+			public void execute(final ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+				editor.moveTo(editor.getDocument().getStartPosition().moveBy(1));
+			}
+		});
+		addKey(keyMap, CHAR_NONE, SWT.HOME, SWT.SHIFT | SWT.CONTROL, new IVexWidgetHandler() {
+			@Override
+			public void execute(final ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+				editor.moveTo(editor.getDocument().getStartPosition().moveBy(1), true);
+			}
+		});
+
+		// (Shift) Page Up/Down
+		addKey(keyMap, CHAR_NONE, SWT.PAGE_DOWN, SWT.NONE, new IVexWidgetHandler() {
+			@Override
+			public void execute(final ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+				editor.moveToNextPage(false);
+			}
+		});
+		addKey(keyMap, CHAR_NONE, SWT.PAGE_DOWN, SWT.SHIFT, new IVexWidgetHandler() {
+			@Override
+			public void execute(final ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+				editor.moveToNextPage(true);
+			}
+		});
+		addKey(keyMap, CHAR_NONE, SWT.PAGE_UP, SWT.NONE, new IVexWidgetHandler() {
+			@Override
+			public void execute(final ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+				editor.moveToPreviousPage(false);
+			}
+		});
+		addKey(keyMap, CHAR_NONE, SWT.PAGE_UP, SWT.SHIFT, new IVexWidgetHandler() {
+			@Override
+			public void execute(final ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+				editor.moveToPreviousPage(true);
+			}
+		});
+
+		return keyMap;
+	}
+
+	/*
+	 * Inner Classes
+	 */
+
+	private final class ViewPort implements IViewPort {
+		@Override
+		public void reconcile(final int maximumHeight) {
+			final int pageSize = getClientArea().height;
+			final int selection = getVerticalBar().getSelection();
+			getVerticalBar().setValues(selection, 0, maximumHeight, pageSize, pageSize / 4, pageSize);
+		}
+
+		@Override
+		public void moveRelative(final int delta) {
+			if (delta == 0) {
+				return;
+			}
+
+			final int selection = getVerticalBar().getSelection() + delta;
+			getVerticalBar().setSelection(selection);
+		}
+
+		@Override
+		public Rectangle getVisibleArea() {
+			return new Rectangle(0, getVerticalBar().getSelection(), getSize().x, getSize().y);
+		}
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/DoubleBufferedRenderer.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/DoubleBufferedRenderer.java
new file mode 100644
index 0000000..deed4c2
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/DoubleBufferedRenderer.java
@@ -0,0 +1,175 @@
+/*******************************************************************************
+ * 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.widget.swt;
+
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.PaintEvent;
+import org.eclipse.swt.events.PaintListener;
+import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Scrollable;
+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.internal.widget.IRenderer;
+
+/**
+ * This class implements double buffering with a dedicated render thread for SWT. This prevents flickering and keeps the
+ * UI responsive.<br/>
+ *
+ * <b>CAUTION:</b> The prevention of flickering works only in conjunction with the style bit SWT.NO_BACKGROUND.
+ *
+ * @see http://git.eclipse.org/c/platform/eclipse.platform.swt.git/tree/examples/org.eclipse.swt.snippets/src/org
+ *      /eclipse/swt/snippets/Snippet48.java
+ * @author Florian Thienel
+ */
+public class DoubleBufferedRenderer implements IRenderer {
+
+	private final Scrollable control;
+
+	private final Object bufferMonitor = new Object();
+	private final RenderBuffer[] buffer = new RenderBuffer[2];
+	private int visibleIndex = 0;
+
+	private final Map<URL, SwtImage> imageCache = new HashMap<URL, SwtImage>();
+
+	public DoubleBufferedRenderer(final Scrollable control) {
+		this.control = control;
+		control.addDisposeListener(new DisposeListener() {
+			@Override
+			public void widgetDisposed(final DisposeEvent e) {
+				DoubleBufferedRenderer.this.widgetDisposed(e);
+			}
+		});
+		control.addPaintListener(new PaintListener() {
+			@Override
+			public void paintControl(final PaintEvent e) {
+				DoubleBufferedRenderer.this.paintControl(e);
+			}
+		});
+	}
+
+	private void widgetDisposed(final DisposeEvent e) {
+		synchronized (bufferMonitor) {
+			for (int i = 0; i < buffer.length; i += 1) {
+				if (buffer[i] != null) {
+					buffer[i].dispose();
+				}
+				buffer[i] = null;
+			}
+		}
+	}
+
+	private void paintControl(final PaintEvent event) {
+		event.gc.drawImage(getVisibleImage(), 0, 0);
+	}
+
+	private Image getVisibleImage() {
+		synchronized (bufferMonitor) {
+			if (buffer[visibleIndex] == null) {
+				buffer[visibleIndex] = createRenderBuffer(Rectangle.NULL);
+			}
+			return buffer[visibleIndex].image;
+		}
+	}
+
+	private RenderBuffer getRenderBuffer(final Rectangle viewPort) {
+		synchronized (bufferMonitor) {
+			final int index = (visibleIndex + 1) % 2;
+			if (buffer[index] == null) {
+				buffer[index] = createRenderBuffer(viewPort);
+			} else if (!viewPortFitsIntoImage(viewPort, buffer[index].image)) {
+				buffer[index].dispose();
+				buffer[index] = createRenderBuffer(viewPort);
+			}
+			return buffer[index];
+		}
+	}
+
+	private static boolean viewPortFitsIntoImage(final Rectangle viewPort, final Image image) {
+		return image.getBounds().contains(viewPort.getWidth() - 1, viewPort.getHeight() - 1);
+	}
+
+	@Override
+	public void render(final Rectangle viewPort, final IRenderStep... steps) {
+		final RenderBuffer buffer = getRenderBuffer(viewPort);
+
+		buffer.graphics.resetOrigin();
+		clearViewPort(viewPort, buffer.graphics);
+		moveOriginToViewPort(viewPort, buffer.graphics);
+
+		for (final IRenderStep step : steps) {
+			try {
+				step.render(buffer.graphics);
+			} catch (final Throwable t) {
+				t.printStackTrace(); //TODO proper logging
+			}
+		}
+
+		makeRenderedImageVisible(buffer.image);
+	}
+
+	private RenderBuffer createRenderBuffer(final Rectangle viewPort) {
+		return new RenderBuffer(control.getDisplay(), viewPort.getWidth(), viewPort.getHeight(), imageCache);
+	}
+
+	private void moveOriginToViewPort(final Rectangle viewPort, final Graphics graphics) {
+		graphics.moveOrigin(0, -viewPort.getY());
+	}
+
+	private void clearViewPort(final Rectangle viewPort, final Graphics graphics) {
+		graphics.setColor(graphics.getColor(Color.WHITE));
+		graphics.fillRect(0, 0, viewPort.getWidth(), viewPort.getHeight());
+	}
+
+	private void makeRenderedImageVisible(final Image newImage) {
+		swapBufferImage(newImage);
+		control.getDisplay().syncExec(new Runnable() {
+			@Override
+			public void run() {
+				control.redraw();
+			}
+		});
+	}
+
+	private void swapBufferImage(final Image newImage) {
+		synchronized (bufferMonitor) {
+			visibleIndex = (visibleIndex + 1) % 2;
+		}
+	}
+
+	private static class RenderBuffer {
+		public final Image image;
+		public final GC gc;
+		public final SwtGraphics graphics;
+
+		public RenderBuffer(final Device device, final int width, final int height, final Map<URL, SwtImage> imageCache) {
+			image = new Image(device, Math.max(1, width), Math.max(1, height));
+			gc = new GC(image);
+			gc.setAdvanced(true);
+			gc.setAntialias(SWT.ON);
+			graphics = new SwtGraphics(gc, imageCache);
+		}
+
+		public void dispose() {
+			graphics.dispose();
+			gc.dispose();
+			image.dispose();
+		}
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/IVexSelection.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/IVexSelection.java
new file mode 100644
index 0000000..cf07ec8
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/IVexSelection.java
@@ -0,0 +1,25 @@
+/*******************************************************************************
+ * 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.widget.swt;
+
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.vex.core.provisional.dom.ContentRange;
+
+/**
+ * @author Florian Thienel
+ */
+public interface IVexSelection extends IStructuredSelection {
+
+	int getCaretOffset();
+
+	ContentRange getSelectedRange();
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/IVexWidgetHandler.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/IVexWidgetHandler.java
index bf5c047..508003b 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/IVexWidgetHandler.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/IVexWidgetHandler.java
@@ -10,7 +10,9 @@
  *******************************************************************************/
 package org.eclipse.vex.core.internal.widget.swt;
 
+import org.eclipse.core.commands.ExecutionEvent;
 import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.vex.core.internal.widget.IDocumentEditor;
 
 /**
  * Interface implemented by handler objects that can act on a {@link VexWidget}.
@@ -19,12 +21,13 @@
 
 	/**
 	 * Executes handler at the specified {@link VexWidget}.
-	 *
+	 * @param event TODO
 	 * @param event
 	 *            the {@link VexWidget} at which to execute handler
+	 *
 	 * @throws ExecutionException
 	 *             if an exception occurred during execution
 	 */
-	void execute(VexWidget widget) throws ExecutionException;
+	void execute(ExecutionEvent event, IDocumentEditor editor) throws ExecutionException;
 
 }
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/SwtClipboard.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/SwtClipboard.java
new file mode 100644
index 0000000..4937eeb
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/SwtClipboard.java
@@ -0,0 +1,105 @@
+/*******************************************************************************
+ * 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.dnd.Clipboard;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.dnd.TransferData;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.vex.core.internal.io.XMLFragment;
+import org.eclipse.vex.core.internal.widget.IClipboard;
+import org.eclipse.vex.core.internal.widget.IDocumentEditor;
+import org.eclipse.vex.core.provisional.dom.DocumentValidationException;
+import org.eclipse.vex.core.provisional.dom.IDocumentFragment;
+
+public class SwtClipboard implements IClipboard {
+
+	private final Clipboard clipboard;
+
+	public SwtClipboard(final Display display) {
+		clipboard = new Clipboard(display);
+	}
+
+	@Override
+	public void dispose() {
+		clipboard.dispose();
+	}
+
+	@Override
+	public void cutSelection(final IDocumentEditor editor) {
+		copySelection(editor);
+		editor.deleteSelection();
+	}
+
+	@Override
+	public void copySelection(final IDocumentEditor editor) {
+		if (!editor.hasSelection()) {
+			return;
+		}
+
+		final String text = editor.getSelectedText();
+		if (text.isEmpty()) {
+			// Some elements (like XInclude) may not contain textual content.
+			final Object[] data = { editor.getSelectedFragment() };
+			final Transfer[] transfers = { DocumentFragmentTransfer.getInstance() };
+			clipboard.setContents(data, transfers);
+		} else {
+			final Object[] data = { editor.getSelectedFragment(), editor.getSelectedText() };
+			final Transfer[] transfers = { DocumentFragmentTransfer.getInstance(), TextTransfer.getInstance() };
+			clipboard.setContents(data, transfers);
+		}
+	}
+
+	@Override
+	public boolean hasContent() {
+		final TransferData[] availableTypes = clipboard.getAvailableTypes();
+		for (final TransferData availableType : availableTypes) {
+			if (DocumentFragmentTransfer.getInstance().isSupportedType(availableType)) {
+				return true;
+			}
+			if (TextTransfer.getInstance().isSupportedType(availableType)) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	@Override
+	public void paste(final IDocumentEditor editor) throws DocumentValidationException {
+		final IDocumentFragment fragment = (IDocumentFragment) clipboard.getContents(DocumentFragmentTransfer.getInstance());
+		if (fragment != null) {
+			editor.insertXML(new XMLFragment(fragment).getXML());
+		} else {
+			pasteText(editor);
+		}
+	}
+
+	@Override
+	public boolean hasTextContent() {
+		final TransferData[] availableTypes = clipboard.getAvailableTypes();
+		for (final TransferData availableType : availableTypes) {
+			if (TextTransfer.getInstance().isSupportedType(availableType)) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	@Override
+	public void pasteText(final IDocumentEditor editor) throws DocumentValidationException {
+		final String text = (String) clipboard.getContents(TextTransfer.getInstance());
+		if (text != null) {
+			editor.insertText(text);
+		}
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/SwtGraphics.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/SwtGraphics.java
index 3b2f93c..d219441 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/SwtGraphics.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/SwtGraphics.java
@@ -8,6 +8,7 @@
  * Contributors:
  *     John Krasnay - initial API and implementation
  *     Mohamadou Nassourou - Bug 298912 - rudimentary support for images
+ *     Florian Thienel - font cache, color cache
  *******************************************************************************/
 package org.eclipse.vex.core.internal.widget.swt;
 
@@ -15,6 +16,8 @@
 import java.io.InputStream;
 import java.net.URL;
 import java.text.MessageFormat;
+import java.util.HashMap;
+import java.util.Map;
 
 import org.eclipse.core.runtime.Assert;
 import org.eclipse.core.runtime.IStatus;
@@ -34,74 +37,161 @@
 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;
 
 /**
  * Implementation of the Vex Graphics interface, mapping it to a org.eclipse.swt.graphics.GC object.
- *
- * <p>
- * The GC given to us by SWT is that of the Canvas, which is just a viewport into the document. This class therefore
- * implements an "origin", which represents the top-left corner of the document relative to the top-left corner of the
- * canvas. The x- and y-coordinates of the origin are always negative.
- * </p>
- * .
  */
 public class SwtGraphics implements Graphics {
 
 	private final GC gc;
-	private int originX;
-	private int originY;
+	private final Map<URL, SwtImage> imageCache;
+
+	private int offsetX;
+	private int offsetY;
+
+	private final HashMap<FontSpec, FontResource> fonts = new HashMap<FontSpec, FontResource>();
+	private final HashMap<Color, ColorResource> colors = new HashMap<Color, ColorResource>();
+	private final HashMap<URL, org.eclipse.swt.graphics.Image> images = new HashMap<URL, org.eclipse.swt.graphics.Image>();
+
+	private SwtFont currentFont;
+	private SwtFontMetrics currentFontMetrics;
+	private LineStyle lineStyle = LineStyle.SOLID;
 
 	/**
-	 * Class constructor.
-	 *
 	 * @param gc
 	 *            SWT GC to which we are drawing.
 	 */
+	@Deprecated
 	public SwtGraphics(final GC gc) {
+		this(gc, new HashMap<URL, SwtImage>());
+	}
+
+	/**
+	 * @param gc
+	 *            SWT GC to which we are drawing.
+	 */
+	public SwtGraphics(final GC gc, final Map<URL, SwtImage> imageCache) {
 		this.gc = gc;
+		this.imageCache = imageCache;
+
+		currentFont = new SwtFont(gc.getFont());
 	}
 
 	@Override
 	public void dispose() {
+		for (final FontResource font : fonts.values()) {
+			font.dispose();
+		}
+		fonts.clear();
+		for (final ColorResource color : colors.values()) {
+			color.dispose();
+		}
+		colors.clear();
+		for (final org.eclipse.swt.graphics.Image image : images.values()) {
+			image.dispose();
+		}
+		images.clear();
+
+		// TODO should not dispose something that comes from outside!
 		gc.dispose();
 	}
 
 	@Override
+	public void resetOrigin() {
+		offsetX = 0;
+		offsetY = 0;
+	}
+
+	@Override
+	public void moveOrigin(final int offsetX, final int offsetY) {
+		this.offsetX += offsetX;
+		this.offsetY += offsetY;
+	}
+
+	@Override
+	public int asAbsoluteX(final int relativeX) {
+		return relativeX + offsetX;
+	}
+
+	@Override
+	public int asAbsoluteY(final int relativeY) {
+		return relativeY + offsetY;
+	}
+
+	@Override
+	public int asRelativeX(final int absoluteX) {
+		return absoluteX - offsetX;
+	}
+
+	@Override
+	public int asRelativeY(final int absoluteY) {
+		return absoluteY - offsetY;
+	}
+
+	@Override
 	public void drawChars(final char[] chars, final int offset, final int length, final int x, final int y) {
 		drawString(new String(chars, offset, length), x, y);
-
 	}
 
 	@Override
 	public void drawLine(final int x1, final int y1, final int x2, final int y2) {
-		gc.drawLine(x1 + originX, y1 + originY, x2 + originX, y2 + originY);
+		gc.drawLine(x1 + offsetX, y1 + offsetY, x2 + offsetX, y2 + offsetY);
 	}
 
 	@Override
 	public void drawOval(final int x, final int y, final int width, final int height) {
-		gc.drawOval(x + originX, y + originY, width, height);
+		gc.drawOval(x + offsetX, y + offsetY, width, height);
 	}
 
 	@Override
 	public void drawRect(final int x, final int y, final int width, final int height) {
-		gc.drawRectangle(x + originX, y + originY, width, height);
+		gc.drawRectangle(x + offsetX, y + offsetY, width, height);
+	}
+
+	@Override
+	public void drawRoundRect(final int x, final int y, final int width, final int height, final int arcWidth, final int arcHeight) {
+		gc.drawRoundRectangle(x + offsetX, y + offsetY, width, height, arcWidth, arcHeight);
+	}
+
+	@Override
+	public void drawPolygon(final int... coordinates) {
+		gc.drawPolygon(translateCoordinates(coordinates));
+	}
+
+	private int[] translateCoordinates(final int... coordinates) {
+		final int[] transformedCoordinates = new int[coordinates.length];
+		for (int i = 0; i < coordinates.length; i += 1) {
+			if (i % 2 == 0) {
+				transformedCoordinates[i] = coordinates[i] + offsetX;
+			} else {
+				transformedCoordinates[i] = coordinates[i] + offsetY;
+			}
+		}
+		return transformedCoordinates;
 	}
 
 	@Override
 	public void drawString(final String s, final int x, final int y) {
-		gc.drawString(s, x + originX, y + originY, true);
+		gc.drawString(s, x + offsetX, y + offsetY, true);
 	}
 
 	@Override
 	public void drawImage(final Image image, final int x, final int y, final int width, final int height) {
 		Assert.isTrue(image instanceof SwtImage);
-		final org.eclipse.swt.graphics.Image swtImage = new org.eclipse.swt.graphics.Image(gc.getDevice(), ((SwtImage) image).imageData);
-		try {
-			gc.drawImage(swtImage, 0, 0, image.getWidth(), image.getHeight(), x + originX, y + originY, width, height);
-		} finally {
-			swtImage.dispose();
+		final org.eclipse.swt.graphics.Image swtImage = toSWT((SwtImage) image);
+		gc.drawImage(swtImage, 0, 0, image.getWidth(), image.getHeight(), x + offsetX, y + offsetY, width, height);
+	}
+
+	private org.eclipse.swt.graphics.Image toSWT(final SwtImage image) {
+		final org.eclipse.swt.graphics.Image cachedImage = images.get(image.url);
+		if (cachedImage != null) {
+			return cachedImage;
 		}
+		final org.eclipse.swt.graphics.Image newImage = new org.eclipse.swt.graphics.Image(gc.getDevice(), image.imageData);
+		images.put(image.url, newImage);
+		return newImage;
 	}
 
 	/**
@@ -110,7 +200,7 @@
 	 */
 	@Override
 	public void fillOval(final int x, final int y, final int width, final int height) {
-		gc.fillOval(x + originX, y + originY, width, height);
+		gc.fillOval(x + offsetX, y + offsetY, width, height);
 	}
 
 	/**
@@ -119,32 +209,50 @@
 	 */
 	@Override
 	public void fillRect(final int x, final int y, final int width, final int height) {
-		gc.fillRectangle(x + originX, y + originY, width, height);
+		gc.fillRectangle(x + offsetX, y + offsetY, width, height);
+	}
+
+	@Override
+	public void fillRoundRect(final int x, final int y, final int width, final int height, final int arcWidth, final int arcHeight) {
+		gc.fillRoundRectangle(x + offsetX, y + offsetY, width, height, arcWidth, arcHeight);
+	}
+
+	@Override
+	public void fillPolygon(final int... coordinates) {
+		gc.fillPolygon(translateCoordinates(coordinates));
 	}
 
 	@Override
 	public Rectangle getClipBounds() {
 		final org.eclipse.swt.graphics.Rectangle r = gc.getClipping();
-		return new Rectangle(r.x - originX, r.y - originY, r.width, r.height);
+		return new Rectangle(r.x - offsetX, r.y - offsetY, r.width, r.height);
 	}
 
 	@Override
-	public ColorResource getColor() {
-		return new SwtColor(gc.getForeground());
+	public FontResource getCurrentFont() {
+		return currentFont;
 	}
 
 	@Override
-	public FontResource getFont() {
-		return new SwtFont(gc.getFont());
+	public FontResource setCurrentFont(final FontResource font) {
+		if (font == currentFont) {
+			return currentFont;
+		}
+
+		final FontResource oldFont = getCurrentFont();
+		currentFont = (SwtFont) font;
+		gc.setFont(currentFont.getSwtFont());
+		currentFontMetrics = new SwtFontMetrics(gc.getFontMetrics());
+		return oldFont;
 	}
 
 	@Override
 	public FontMetrics getFontMetrics() {
-		return new SwtFontMetrics(gc.getFontMetrics());
+		return currentFontMetrics;
 	}
 
 	@Override
-	public int getLineStyle() {
+	public LineStyle getLineStyle() {
 		return lineStyle;
 	}
 
@@ -155,14 +263,26 @@
 
 	@Override
 	public Image getImage(final URL url) {
+		final SwtImage cachedImage = imageCache.get(url);
+		if (cachedImage != null) {
+			return cachedImage;
+		}
+
 		final ImageData[] imageData = loadImageData(url);
 		if (imageData != null && imageData.length > 0) {
-			return new SwtImage(imageData[0]);
+			final SwtImage loadedImage = new SwtImage(url, imageData[0]);
+			imageCache.put(url, loadedImage);
+			return loadedImage;
 		}
-		return new SwtImage(Display.getDefault().getSystemImage(SWT.ICON_ERROR).getImageData());
+
+		return new SwtImage(url, Display.getDefault().getSystemImage(SWT.ICON_ERROR).getImageData());
 	}
 
 	private static ImageData[] loadImageData(final URL url) {
+		if (url == null) {
+			return null;
+		}
+
 		final ImageLoader imageLoader = new ImageLoader();
 		try {
 			final InputStream in = url.openStream();
@@ -190,6 +310,11 @@
 	}
 
 	@Override
+	public ColorResource getColor() {
+		return getForeground();
+	}
+
+	@Override
 	public ColorResource setColor(final ColorResource color) {
 		final ColorResource oldColor = getColor();
 		gc.setForeground(((SwtColor) color).getSwtColor());
@@ -198,20 +323,42 @@
 	}
 
 	@Override
-	public FontResource setFont(final FontResource font) {
-		final FontResource oldFont = getFont();
-		gc.setFont(((SwtFont) font).getSwtFont());
-		return oldFont;
+	public ColorResource getForeground() {
+		return new SwtColor(gc.getForeground());
 	}
 
 	@Override
-	public void setLineStyle(final int lineStyle) {
-		this.lineStyle = lineStyle;
-		switch (lineStyle) {
-		case LINE_DASH:
+	public ColorResource setForeground(final ColorResource color) {
+		final ColorResource oldColor = getForeground();
+		gc.setForeground(((SwtColor) color).getSwtColor());
+		return oldColor;
+	}
+
+	@Override
+	public ColorResource getBackground() {
+		return new SwtColor(gc.getBackground());
+	}
+
+	@Override
+	public ColorResource setBackground(final ColorResource color) {
+		final ColorResource oldColor = getBackground();
+		gc.setBackground(((SwtColor) color).getSwtColor());
+		return oldColor;
+	}
+
+	@Override
+	public void swapColors() {
+		setForeground(setBackground(getForeground()));
+	}
+
+	@Override
+	public void setLineStyle(final LineStyle style) {
+		lineStyle = style;
+		switch (style) {
+		case DASHED:
 			gc.setLineStyle(SWT.LINE_DASH);
 			break;
-		case LINE_DOT:
+		case DOTTED:
 			gc.setLineStyle(SWT.LINE_DOT);
 			break;
 		default:
@@ -231,12 +378,30 @@
 	}
 
 	@Override
-	public ColorResource createColor(final Color rgb) {
+	public ColorResource getColor(final Color rgb) {
+		ColorResource color = colors.get(rgb);
+		if (color == null) {
+			color = createColor(rgb);
+			colors.put(rgb, color);
+		}
+		return color;
+	}
+
+	private SwtColor createColor(final Color rgb) {
 		return new SwtColor(new org.eclipse.swt.graphics.Color(null, rgb.getRed(), rgb.getGreen(), rgb.getBlue()));
 	}
 
 	@Override
-	public FontResource createFont(final FontSpec fontSpec) {
+	public FontResource getFont(final FontSpec fontSpec) {
+		FontResource font = fonts.get(fontSpec);
+		if (font == null) {
+			font = createFont(fontSpec);
+			fonts.put(fontSpec, font);
+		}
+		return font;
+	}
+
+	private FontResource createFont(final FontSpec fontSpec) {
 		int style = SWT.NORMAL;
 		if ((fontSpec.getStyle() & FontSpec.BOLD) > 0) {
 			style |= SWT.BOLD;
@@ -245,9 +410,7 @@
 			style |= SWT.ITALIC;
 		}
 		final int size = Math.round(fontSpec.getSize() * 72 / 90); // TODO: fix. SWT
-		// uses pts, AWT
-		// uses device
-		// units
+		// uses pts, AWT uses device units
 		final String[] names = fontSpec.getNames();
 		final FontData[] fd = new FontData[names.length];
 		for (int i = 0; i < names.length; i++) {
@@ -268,26 +431,9 @@
 		}
 	}
 
-	/**
-	 * Sets the origin of this graphics object. See the class description for more details.
-	 *
-	 * @param x
-	 *            x-coordinate of the origin, relative to the viewport.
-	 * @param y
-	 *            y-coordinate of the origin, relative to the viewport.
-	 */
-	public void setOrigin(final int x, final int y) {
-		originX = x;
-		originY = y;
-	}
-
 	@Override
 	public int stringWidth(final String s) {
 		return gc.stringExtent(s).x;
 	}
 
-	// ========================================================== PRIVATE
-
-	private int lineStyle = LINE_SOLID;
-
 }
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/SwtImage.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/SwtImage.java
index 6e9edc4..2987b04 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/SwtImage.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/SwtImage.java
@@ -1,36 +1,40 @@
-/*******************************************************************************

- * Copyright (c) 2010 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.graphics.ImageData;

-import org.eclipse.vex.core.internal.core.Image;

-

-/**

- * @author Florian Thienel

- */

-public class SwtImage implements Image {

-

-	public final ImageData imageData;

-

-	public SwtImage(final ImageData imageData) {

-		this.imageData = imageData;

-	}

-

-	@Override

-	public int getHeight() {

-		return imageData.height;

-	}

-

-	@Override

-	public int getWidth() {

-		return imageData.width;

-	}

-}

+/*******************************************************************************
+ * 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
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * 		Florian Thienel - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.vex.core.internal.widget.swt;
+
+import java.net.URL;
+
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.vex.core.internal.core.Image;
+
+/**
+ * @author Florian Thienel
+ */
+public class SwtImage implements Image {
+
+	public final URL url;
+	public final ImageData imageData;
+
+	public SwtImage(final URL url, final ImageData imageData) {
+		this.url = url;
+		this.imageData = imageData;
+	}
+
+	@Override
+	public int getHeight() {
+		return imageData.height;
+	}
+
+	@Override
+	public int getWidth() {
+		return imageData.width;
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/VexSelection.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/VexSelection.java
new file mode 100644
index 0000000..5e19928
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/VexSelection.java
@@ -0,0 +1,90 @@
+/*******************************************************************************
+ * 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 java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import org.eclipse.vex.core.internal.widget.IDocumentEditor;
+import org.eclipse.vex.core.provisional.dom.ContentRange;
+import org.eclipse.vex.core.provisional.dom.IDocumentFragment;
+import org.eclipse.vex.core.provisional.dom.INode;
+
+/**
+ * @author Florian Thienel
+ */
+public class VexSelection implements IVexSelection {
+
+	private final int caretOffset;
+	private final ContentRange selectedRange;
+	private final IDocumentFragment selectedFragment;
+	private final INode currentNode;
+
+	public VexSelection(final IDocumentEditor editor) {
+		caretOffset = editor.getCaretPosition().getOffset();
+		selectedRange = editor.getSelectedRange();
+		selectedFragment = editor.getSelectedFragment();
+		currentNode = editor.getCurrentNode();
+	}
+
+	@Override
+	public boolean isEmpty() {
+		return false;
+	}
+
+	@Override
+	public int getCaretOffset() {
+		return caretOffset;
+	}
+
+	@Override
+	public ContentRange getSelectedRange() {
+		return selectedRange;
+	}
+
+	public INode getFirstElement() {
+		if (!isStructureSelected()) {
+			return currentNode;
+		}
+		return selectedFragment.children().first();
+	}
+
+	private boolean isStructureSelected() {
+		return selectedFragment != null && !selectedFragment.children().withoutText().isEmpty();
+	}
+
+	public Iterator<INode> iterator() {
+		if (!isStructureSelected()) {
+			return Collections.singletonList(currentNode).iterator();
+		}
+		return selectedFragment.children().iterator();
+	}
+
+	public int size() {
+		if (!isStructureSelected()) {
+			return 1;
+		}
+		return selectedFragment.children().count();
+	}
+
+	public Object[] toArray() {
+		return toList().toArray();
+	}
+
+	public List<INode> toList() {
+		if (!isStructureSelected()) {
+			return Collections.singletonList(currentNode);
+		}
+		return selectedFragment.children().asList();
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/VexWidget.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/VexWidget.java
index 1866055..e251613 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/VexWidget.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/VexWidget.java
@@ -16,6 +16,7 @@
 import java.io.IOException;
 import java.net.URL;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -23,6 +24,7 @@
 import javax.swing.undo.CannotRedoException;
 import javax.swing.undo.CannotUndoException;
 
+import org.eclipse.core.commands.ExecutionEvent;
 import org.eclipse.core.commands.ExecutionException;
 import org.eclipse.core.runtime.QualifiedName;
 import org.eclipse.jface.viewers.ISelection;
@@ -66,7 +68,9 @@
 import org.eclipse.vex.core.internal.css.StyleSheet;
 import org.eclipse.vex.core.internal.io.XMLFragment;
 import org.eclipse.vex.core.internal.widget.BaseVexWidget;
+import org.eclipse.vex.core.internal.widget.IDocumentEditor;
 import org.eclipse.vex.core.internal.widget.IHostComponent;
+import org.eclipse.vex.core.internal.widget.ITableModel;
 import org.eclipse.vex.core.internal.widget.IVexWidget;
 import org.eclipse.vex.core.internal.widget.ReadOnlyException;
 import org.eclipse.vex.core.provisional.dom.ContentPosition;
@@ -135,11 +139,6 @@
 	}
 
 	@Override
-	public void beginWork() {
-		impl.beginWork();
-	}
-
-	@Override
 	public boolean canInsertComment() {
 		return impl.canInsertComment();
 	}
@@ -155,9 +154,6 @@
 		return false;
 	}
 
-	/**
-	 * @see org.eclipse.vex.core.internal.widget.IVexWidget#canPasteText()
-	 */
 	@Override
 	public boolean canPasteText() {
 		// TODO Auto-generated method stub
@@ -214,13 +210,13 @@
 	}
 
 	@Override
-	public void deleteNextChar() throws DocumentValidationException {
-		impl.deleteNextChar();
+	public void deleteForward() throws DocumentValidationException {
+		impl.deleteForward();
 	}
 
 	@Override
-	public void deletePreviousChar() throws DocumentValidationException {
-		impl.deletePreviousChar();
+	public void deleteBackward() throws DocumentValidationException {
+		impl.deleteBackward();
 	}
 
 	@Override
@@ -244,11 +240,6 @@
 	}
 
 	@Override
-	public void endWork(final boolean success) {
-		impl.endWork(success);
-	}
-
-	@Override
 	public ContentPosition getCaretPosition() {
 		return impl.getCaretPosition();
 	}
@@ -319,6 +310,11 @@
 	}
 
 	@Override
+	public void insertLineBreak() throws DocumentValidationException {
+		impl.insertLineBreak();
+	}
+
+	@Override
 	public boolean canInsertFragment(final IDocumentFragment fragment) {
 		return impl.canInsertFragment(fragment);
 	}
@@ -562,6 +558,11 @@
 	}
 
 	@Override
+	public void setDocument(final IDocument document) {
+		impl.setDocument(document);
+	}
+
+	@Override
 	public void setLayoutWidth(final int width) {
 		impl.setLayoutWidth(width);
 	}
@@ -586,6 +587,16 @@
 	}
 
 	@Override
+	public void setTableModel(final ITableModel tableModel) {
+		impl.setTableModel(tableModel);
+	}
+
+	@Override
+	public ITableModel getTableModel() {
+		return impl.getTableModel();
+	}
+
+	@Override
 	public boolean canSplit() {
 		return impl.canSplit();
 	}
@@ -601,6 +612,16 @@
 	}
 
 	@Override
+	public boolean isDirty() {
+		return impl.isDirty();
+	}
+
+	@Override
+	public void markClean() {
+		impl.markClean();
+	}
+
+	@Override
 	public ContentPosition viewToModel(final int x, final int y) {
 		return impl.viewToModel(x, y);
 	}
@@ -774,11 +795,11 @@
 	private static abstract class Action implements IVexWidgetHandler {
 
 		@Override
-		public void execute(final VexWidget widget) throws ExecutionException {
-			runEx(widget);
+		public void execute(final ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+			runEx(editor);
 		}
 
-		public abstract void runEx(IVexWidget w) throws ExecutionException;
+		public abstract void runEx(IDocumentEditor editor) throws ExecutionException;
 
 	}
 
@@ -790,7 +811,7 @@
 			final IVexWidgetHandler handler = keyMap.get(keyStroke);
 			if (handler != null) {
 				try {
-					handler.execute(VexWidget.this);
+					handler.execute(new ExecutionEvent(null, Collections.emptyMap(), event, null), VexWidget.this);
 				} catch (final ReadOnlyException e) {
 					// TODO give feedback: the editor is read-only
 				} catch (final Exception ex) {
@@ -846,19 +867,18 @@
 		public void paintControl(final PaintEvent e) {
 
 			final SwtGraphics g = new SwtGraphics(e.gc);
-			g.setOrigin(originX, originY);
+			g.moveOrigin(originX, originY);
 
 			Color bgColor = impl.getBackgroundColor();
 			if (bgColor == null) {
 				bgColor = new Color(255, 255, 255);
 			}
 
-			final ColorResource color = g.createColor(bgColor);
+			final ColorResource color = g.getColor(bgColor);
 			final ColorResource oldColor = g.setColor(color);
 			final Rectangle r = g.getClipBounds();
 			g.fillRect(r.getX(), r.getY(), r.getWidth(), r.getHeight());
 			g.setColor(oldColor);
-			color.dispose();
 
 			impl.paint(g, 0, 0);
 		}
@@ -902,83 +922,83 @@
 		// arrows: (Shift) Up/Down, {-, Shift, Ctrl, Shift+Ctrl} + Left/Right
 		addKey(CHAR_NONE, SWT.ARROW_DOWN, SWT.NONE, new Action() {
 			@Override
-			public void runEx(final IVexWidget w) {
-				w.moveToNextLine(false);
+			public void runEx(final IDocumentEditor editor) {
+				editor.moveToNextLine(false);
 			}
 		});
 		addKey(CHAR_NONE, SWT.ARROW_DOWN, SWT.SHIFT, new Action() {
 			@Override
-			public void runEx(final IVexWidget w) {
-				w.moveToNextLine(true);
+			public void runEx(final IDocumentEditor editor) {
+				editor.moveToNextLine(true);
 			}
 		});
 		addKey(CHAR_NONE, SWT.ARROW_UP, SWT.NONE, new Action() {
 			@Override
-			public void runEx(final IVexWidget w) {
-				w.moveToPreviousLine(false);
+			public void runEx(final IDocumentEditor editor) {
+				editor.moveToPreviousLine(false);
 			}
 		});
 		addKey(CHAR_NONE, SWT.ARROW_UP, SWT.SHIFT, new Action() {
 			@Override
-			public void runEx(final IVexWidget w) {
-				w.moveToPreviousLine(true);
+			public void runEx(final IDocumentEditor editor) {
+				editor.moveToPreviousLine(true);
 			}
 		});
 		addKey(CHAR_NONE, SWT.ARROW_LEFT, SWT.NONE, new Action() {
 			@Override
-			public void runEx(final IVexWidget w) {
-				w.moveBy(-1);
+			public void runEx(final IDocumentEditor editor) {
+				editor.moveBy(-1);
 			}
 		});
 		addKey(CHAR_NONE, SWT.ARROW_LEFT, SWT.SHIFT, new Action() {
 			@Override
-			public void runEx(final IVexWidget w) {
-				w.moveBy(-1, true);
+			public void runEx(final IDocumentEditor editor) {
+				editor.moveBy(-1, true);
 			}
 		});
 		addKey(CHAR_NONE, SWT.ARROW_LEFT, SWT.CONTROL, new Action() {
 			@Override
-			public void runEx(final IVexWidget w) {
-				w.moveToPreviousWord(false);
+			public void runEx(final IDocumentEditor editor) {
+				editor.moveToPreviousWord(false);
 			}
 		});
 		addKey(CHAR_NONE, SWT.ARROW_LEFT, SWT.SHIFT | SWT.CONTROL, new Action() {
 			@Override
-			public void runEx(final IVexWidget w) {
-				w.moveToPreviousWord(true);
+			public void runEx(final IDocumentEditor editor) {
+				editor.moveToPreviousWord(true);
 			}
 		});
 		addKey(CHAR_NONE, SWT.ARROW_RIGHT, SWT.NONE, new Action() {
 			@Override
-			public void runEx(final IVexWidget w) {
-				w.moveBy(+1);
+			public void runEx(final IDocumentEditor editor) {
+				editor.moveBy(+1);
 			}
 		});
 		addKey(CHAR_NONE, SWT.ARROW_RIGHT, SWT.SHIFT, new Action() {
 			@Override
-			public void runEx(final IVexWidget w) {
-				w.moveBy(+1, true);
+			public void runEx(final IDocumentEditor editor) {
+				editor.moveBy(+1, true);
 			}
 		});
 		addKey(CHAR_NONE, SWT.ARROW_RIGHT, SWT.CONTROL, new Action() {
 			@Override
-			public void runEx(final IVexWidget w) {
-				w.moveToNextWord(false);
+			public void runEx(final IDocumentEditor editor) {
+				editor.moveToNextWord(false);
 			}
 		});
 		addKey(CHAR_NONE, SWT.ARROW_RIGHT, SWT.SHIFT | SWT.CONTROL, new Action() {
 			@Override
-			public void runEx(final IVexWidget w) {
-				w.moveToNextWord(true);
+			public void runEx(final IDocumentEditor editor) {
+				editor.moveToNextWord(true);
 			}
 		});
 
 		// Delete/Backspace
 		addKey(SWT.BS, SWT.BS, SWT.NONE, new Action() {
 			@Override
-			public void runEx(final IVexWidget w) throws ExecutionException {
+			public void runEx(final IDocumentEditor editor) throws ExecutionException {
 				try {
-					w.deletePreviousChar();
+					editor.deleteBackward();
 				} catch (final DocumentValidationException e) {
 					throw new ExecutionException(e.getMessage(), e);
 				}
@@ -986,9 +1006,9 @@
 		});
 		addKey(SWT.DEL, SWT.DEL, SWT.NONE, new Action() {
 			@Override
-			public void runEx(final IVexWidget w) throws ExecutionException {
+			public void runEx(final IDocumentEditor editor) throws ExecutionException {
 				try {
-					w.deleteNextChar();
+					editor.deleteForward();
 				} catch (final DocumentValidationException e) {
 					throw new ExecutionException(e.getMessage(), e);
 				}
@@ -998,76 +1018,76 @@
 		// {-, Shift, Ctrl, Shift+Ctrl} + Home/End
 		addKey(CHAR_NONE, SWT.END, SWT.NONE, new Action() {
 			@Override
-			public void runEx(final IVexWidget w) {
-				w.moveToLineEnd(false);
+			public void runEx(final IDocumentEditor editor) {
+				editor.moveToLineEnd(false);
 			}
 		});
 		addKey(CHAR_NONE, SWT.END, SWT.SHIFT, new Action() {
 			@Override
-			public void runEx(final IVexWidget w) {
-				w.moveToLineEnd(true);
+			public void runEx(final IDocumentEditor editor) {
+				editor.moveToLineEnd(true);
 			}
 		});
 		addKey(CHAR_NONE, SWT.END, SWT.CONTROL, new Action() {
 			@Override
-			public void runEx(final IVexWidget w) {
-				w.moveTo(w.getDocument().getEndPosition().moveBy(-1));
+			public void runEx(final IDocumentEditor editor) {
+				editor.moveTo(editor.getDocument().getEndPosition().moveBy(-1));
 			}
 		});
 		addKey(CHAR_NONE, SWT.END, SWT.SHIFT | SWT.CONTROL, new Action() {
 			@Override
-			public void runEx(final IVexWidget w) {
-				w.moveTo(w.getDocument().getEndPosition().moveBy(-1));
+			public void runEx(final IDocumentEditor editor) {
+				editor.moveTo(editor.getDocument().getEndPosition().moveBy(-1));
 			}
 		});
 		addKey(CHAR_NONE, SWT.HOME, SWT.NONE, new Action() {
 			@Override
-			public void runEx(final IVexWidget w) {
-				w.moveToLineStart(false);
+			public void runEx(final IDocumentEditor editor) {
+				editor.moveToLineStart(false);
 			}
 		});
 		addKey(CHAR_NONE, SWT.HOME, SWT.SHIFT, new Action() {
 			@Override
-			public void runEx(final IVexWidget w) {
-				w.moveToLineStart(true);
+			public void runEx(final IDocumentEditor editor) {
+				editor.moveToLineStart(true);
 			}
 		});
 		addKey(CHAR_NONE, SWT.HOME, SWT.CONTROL, new Action() {
 			@Override
-			public void runEx(final IVexWidget w) {
-				w.moveTo(w.getDocument().getStartPosition().moveBy(1));
+			public void runEx(final IDocumentEditor editor) {
+				editor.moveTo(editor.getDocument().getStartPosition().moveBy(1));
 			}
 		});
 		addKey(CHAR_NONE, SWT.HOME, SWT.SHIFT | SWT.CONTROL, new Action() {
 			@Override
-			public void runEx(final IVexWidget w) {
-				w.moveTo(w.getDocument().getStartPosition().moveBy(1), true);
+			public void runEx(final IDocumentEditor editor) {
+				editor.moveTo(editor.getDocument().getStartPosition().moveBy(1), true);
 			}
 		});
 
 		// (Shift) Page Up/Down
 		addKey(CHAR_NONE, SWT.PAGE_DOWN, SWT.NONE, new Action() {
 			@Override
-			public void runEx(final IVexWidget w) {
-				w.moveToNextPage(false);
+			public void runEx(final IDocumentEditor editor) {
+				editor.moveToNextPage(false);
 			}
 		});
 		addKey(CHAR_NONE, SWT.PAGE_DOWN, SWT.SHIFT, new Action() {
 			@Override
-			public void runEx(final IVexWidget w) {
-				w.moveToNextPage(true);
+			public void runEx(final IDocumentEditor editor) {
+				editor.moveToNextPage(true);
 			}
 		});
 		addKey(CHAR_NONE, SWT.PAGE_UP, SWT.NONE, new Action() {
 			@Override
-			public void runEx(final IVexWidget w) {
-				w.moveToPreviousPage(false);
+			public void runEx(final IDocumentEditor editor) {
+				editor.moveToPreviousPage(false);
 			}
 		});
 		addKey(CHAR_NONE, SWT.PAGE_UP, SWT.SHIFT, new Action() {
 			@Override
-			public void runEx(final IVexWidget w) {
-				w.moveToPreviousPage(true);
+			public void runEx(final IDocumentEditor editor) {
+				editor.moveToPreviousPage(true);
 			}
 		});
 	}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/provisional/dom/ContentRange.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/provisional/dom/ContentRange.java
index 3b59ee9..1336774 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/provisional/dom/ContentRange.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/provisional/dom/ContentRange.java
@@ -52,6 +52,20 @@
 		}

 	}

 

+	/**

+	 * Create a ContentRange around the given offset with the given window size in each direction. The length of the

+	 * created range is twice the given window size.

+	 *

+	 * @param offset

+	 *            the offset

+	 * @param windowSize

+	 *            the number of characters to include on each side of offset

+	 * @return the range around offset

+	 */

+	public static ContentRange window(final int offset, final int windowSize) {

+		return new ContentRange(offset - windowSize, offset + windowSize);

+	}

+

 	public int getStartOffset() {

 		return startOffset;

 	}

@@ -131,6 +145,15 @@
 		return new ContentRange(startOffset + deltaStart, endOffset + deltaEnd);

 	}

 

+	/**

+	 * Resize this range to fit into the given limiting range.

+	 *

+	 * @return the resized range

+	 */

+	public ContentRange limitTo(final ContentRange limitingRange) {

+		return new ContentRange(Math.max(limitingRange.startOffset, startOffset), Math.min(endOffset, limitingRange.endOffset));

+	}

+

 	@Override

 	public String toString() {

 		return "ContentRange[" + startOffset + ", " + endOffset + "]";

diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/provisional/dom/IContent.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/provisional/dom/IContent.java
index aa396ae..07d3a51 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/provisional/dom/IContent.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/provisional/dom/IContent.java
@@ -77,6 +77,10 @@
 	 */
 	String getRawText();
 
+	void insertLineBreak(final int offset);
+
+	MultilineText getMultilineText(final ContentRange range);
+
 	/**
 	 * Insert the given content into this content at the given offset.
 	 *
@@ -118,6 +122,14 @@
 	boolean isTagMarker(int offset);
 
 	/**
+	 * Indicate if there is a line break at the given offset.
+	 *
+	 * @param offset
+	 *            offset at which to check if a line break is present.
+	 */
+	boolean isLineBreak(int offset);
+
+	/**
 	 * Delete the given range of characters.
 	 *
 	 * @param range
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/provisional/dom/IDocument.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/provisional/dom/IDocument.java
index 376436a..6b72af5 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/provisional/dom/IDocument.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/provisional/dom/IDocument.java
@@ -145,6 +145,8 @@
 	 */

 	void insertText(int offset, String text) throws DocumentValidationException;

 

+	void insertLineBreak(int offset) throws DocumentValidationException;

+

 	/**

 	 * @return true if a comment can be inserted a the given offset

 	 */

diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/provisional/dom/MultilineText.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/provisional/dom/MultilineText.java
new file mode 100644
index 0000000..120e70e
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/provisional/dom/MultilineText.java
@@ -0,0 +1,46 @@
+/*******************************************************************************
+ * 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.provisional.dom;
+
+import java.util.ArrayList;
+
+public class MultilineText {
+
+	private final ArrayList<Line> lines = new ArrayList<Line>();
+
+	public void appendLine(final String text, final ContentRange range) {
+		lines.add(new Line(text, range));
+	}
+
+	public int size() {
+		return lines.size();
+	}
+
+	public String getText(final int lineIndex) {
+		final Line line = lines.get(lineIndex);
+		return line.text;
+	}
+
+	public ContentRange getRange(final int lineIndex) {
+		final Line line = lines.get(lineIndex);
+		return line.range;
+	}
+
+	private static class Line {
+		public final String text;
+		public final ContentRange range;
+
+		public Line(final String text, final ContentRange range) {
+			this.text = text;
+			this.range = range;
+		}
+	}
+}
diff --git a/org.eclipse.vex.dita/css/dita_classed_shell_vex.css b/org.eclipse.vex.dita/css/dita_classed_shell_vex.css
index b6ad999..aedd3fd 100644
--- a/org.eclipse.vex.dita/css/dita_classed_shell_vex.css
+++ b/org.eclipse.vex.dita/css/dita_classed_shell_vex.css
@@ -26,7 +26,7 @@
 }
 
 *[class~="topic\/image"] {
-	background-image: attr(href);
+	content: image(attr(href));
 }
 
 *[class~="topic\/image"][placement="break"] {
diff --git a/org.eclipse.vex.docbook/styles/docbook-plain.css b/org.eclipse.vex.docbook/styles/docbook-plain.css
index 5c52cd7..480452a 100644
--- a/org.eclipse.vex.docbook/styles/docbook-plain.css
+++ b/org.eclipse.vex.docbook/styles/docbook-plain.css
@@ -48,7 +48,7 @@
 }
 
 arg {
-	display: inline;	
+	display: inline;
 }
 
 article,book,chapter,colophon {
@@ -340,7 +340,7 @@
 
 constant {
 	display: inline;
-	font-family: "Courier New", monospace; 
+	font-family: "Courier New", monospace;
 }
 
 copyright {
@@ -495,7 +495,7 @@
 }
 
 formalpara {
-	display: block;	
+	display: block;
 }
 
 formalpara > title {
@@ -576,7 +576,7 @@
 
 graphic {
 	display: block;
-	background-image: attr(fileref);
+	content: image(attr(fileref));
 }
 
 group {
@@ -623,13 +623,15 @@
 	display: inline;
 }
 
-inlinemediaobject>imageobject>imagedata {
-	display: inline;
+info {
+	display: block;
 }
 
 imagedata {
-	display: block;
-	background-image: attr(fileref);
+	display: inline;
+	content: image(attr(fileref));
+	width: attr(width);
+	height: attr(depth);
 }
 
 imageobject {
@@ -668,7 +670,7 @@
 
 inlinegraphic {
 	display: inline;
-	background-image: attr(fileref);
+	content: image(attr(fileref));
 }
 
 inlinemediaobject {
@@ -722,7 +724,7 @@
 
 keycode {
 	display: inline;
-	font-family: "Courier New", monospace; 
+	font-family: "Courier New", monospace;
 }
 
 keycombo {
@@ -987,15 +989,15 @@
 }
 productname[class="registered"]:after {
 	display: inline;
-	content: '®';
+	content: '®';
 }
 productname[class="trade"]:after {
 	display: inline;
-	content: '™';
+	content: '™';
 }
 productname[class="copyright"]:before {
 	display: inline;
-	content: '©';
+	content: '©';
 }
 
 productnumber {
@@ -1166,7 +1168,7 @@
 
 screenshot {
 	display: block;
-	white-space: pre;	
+	white-space: pre;
 }
 
 secondary {
@@ -1247,7 +1249,7 @@
 
 step {
 	display: list-item;
-	list-style-type: decimal; 
+	list-style-type: decimal;
 }
 
 stepalternatives {
@@ -1260,7 +1262,7 @@
 
 substeps {
 	display: list-item;
-	list-style-type: lower-alpha; 
+	list-style-type: lower-alpha;
 }
 
 subtitle {
@@ -1285,7 +1287,7 @@
 }
 
 symbolt {
-	display: inline;	
+	display: inline;
 }
 
 synopsis {
@@ -1300,13 +1302,13 @@
 }
 
 tag {
-	display: inline;	
+	display: inline;
 }
 tag:before {
-	content: '<';	
+	content: '<';
 }
 tag:after {
-	content: '>';	
+	content: '>';
 }
 
 table {
@@ -1362,7 +1364,7 @@
 	/* TODO display referenced text */
 	display: inline;
 	font-size: smaller;
-	background-color: #B0B0B0;	
+	background-color: #B0B0B0;
 }
 
 tip {
@@ -1562,13 +1564,13 @@
 
 wordasword {
 	display: inline;
-	font-style: italic;		
+	font-style: italic;
 }
 
 xref {
 	display: inline;
 	color: blue;
-	font-style: italic;	
+	font-style: italic;
 }
 
 xref:before {
diff --git a/org.eclipse.vex.releng/Vex.launch b/org.eclipse.vex.releng/Vex.launch
index c9c380d..6399222 100644
--- a/org.eclipse.vex.releng/Vex.launch
+++ b/org.eclipse.vex.releng/Vex.launch
@@ -1,33 +1,33 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<launchConfiguration type="org.eclipse.pde.ui.RuntimeWorkbench">
-<booleanAttribute key="append.args" value="true"/>
-<booleanAttribute key="askclear" value="true"/>
-<booleanAttribute key="automaticAdd" value="true"/>
-<booleanAttribute key="automaticValidate" value="false"/>
-<stringAttribute key="bootstrap" value=""/>
-<stringAttribute key="checked" value="[NONE]"/>
-<booleanAttribute key="clearConfig" value="true"/>
-<booleanAttribute key="clearws" value="false"/>
-<booleanAttribute key="clearwslog" value="false"/>
-<stringAttribute key="configLocation" value="${workspace_loc}/.metadata/.plugins/org.eclipse.pde.core/Vex"/>
-<booleanAttribute key="default" value="true"/>
-<stringAttribute key="deselected_workspace_plugins" value="org.eclipse.vex.core.tests,org.eclipse.vex.ui.tests,org.eclipse.vex.tests,org.eclipse.test,ft.vex.development"/>
-<booleanAttribute key="includeOptional" value="true"/>
-<stringAttribute key="location" value="${workspace_loc}/../runtime-vex"/>
-<stringAttribute key="org.eclipse.jdt.launching.JRE_CONTAINER" value="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6"/>
-<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-os ${target.os} -ws ${target.ws} -arch ${target.arch} -nl ${target.nl} -console"/>
-<stringAttribute key="org.eclipse.jdt.launching.SOURCE_PATH_PROVIDER" value="org.eclipse.pde.ui.workbenchClasspathProvider"/>
-<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-verbose:gc"/>
-<stringAttribute key="pde.version" value="3.3"/>
-<stringAttribute key="product" value="org.eclipse.platform.ide"/>
-<stringAttribute key="selected_target_plugins" value="org.eclipse.emf.ecore.change*2.5.0.v200906151043@default:default,org.eclipse.team.core*3.5.1.r35x_20100113-0800@default:default,org.eclipse.team.ui*3.5.0.I20090430-0408@default:default,org.eclipse.core.databinding.property*1.2.0.M20090819-0800@default:default,org.eclipse.update.configurator*3.3.0.v20090312@3:true,org.eclipse.wst.dtd.core*1.1.300.v200904181727@default:default,org.eclipse.ant.core*3.2.101.v20091110_r352@default:default,javax.xml*1.3.4.v201005080400@default:default,org.eclipse.osgi*3.5.2.R35x_v20100126@-1:true,org.eclipse.equinox.preferences*3.2.301.R35x_v20091117@default:default,org.eclipse.equinox.p2.core*1.0.101.R35x_v20090819@default:default,org.eclipse.wst.common.emfworkbench.integration*1.1.301.v200908101600@default:default,org.eclipse.wst.common.uriresolver*1.1.301.v200805140415@default:default,org.eclipse.compare.core*3.5.0.I20090430-0408@default:default,org.eclipse.ui.navigator*3.4.2.M20100120-0800@default:default,javax.activation*1.1.0.v201005080500@default:default,org.eclipse.update.ui*3.2.201.R35x_v20090813@default:default,javax.servlet*2.5.0.v200910301333@default:default,org.eclipse.jface*3.5.2.M20100120-0800@default:default,org.eclipse.ltk.ui.refactoring*3.4.101.r352_v20100209@default:default,org.eclipse.equinox.app*1.2.1.R35x_v20091203@default:default,org.eclipse.wst.common.core*1.1.201.v200806010600@default:default,org.eclipse.core.jobs*3.4.100.v20090429-1800@default:default,org.eclipse.help*3.4.1.v20090805_35x@default:default,org.eclipse.jface.text*3.5.2.r352_v20091118-0800@default:default,org.eclipse.core.variables*3.2.200.v20090521@default:default,org.eclipse.update.core*3.2.300.v20090525@default:default,org.eclipse.ltk.core.refactoring*3.5.0.v20090513-2000@default:default,org.eclipse.equinox.transforms.hook@default:false,org.eclipse.ui.intro*3.3.2.v20100111_35x@default:default,org.slf4j.api@default:default,org.eclipse.wst.common.environment*1.0.301.v200908101600@default:default,org.eclipse.equinox.concurrent*1.0.1.R35x_v20100209@default:default,org.eclipse.core.net.win32.x86_64*1.0.0.I20090306-1030@default:false,org.eclipse.ui.views*3.4.1.M20090826-0800@default:default,org.eclipse.emf.ecore*2.5.0.v200906151043@default:default,org.apache.xerces*2.9.0.v200909240008@default:default,org.eclipse.jface.databinding*1.3.1.M20090826-0800@default:default,org.eclipse.wst.common.emf*1.1.301.v200908181930@default:default,org.eclipse.core.commands*3.5.0.I20090525-2000@default:default,org.eclipse.ui.navigator.resources*3.4.1.M20090826-0800@default:default,com.ibm.icu*4.0.1.v20090822@default:default,org.eclipse.emf.common*2.5.0.v200906151043@default:default,org.eclipse.equinox.http.jetty*2.0.0.v20090520-1800@default:default,org.eclipse.ui.cheatsheets*3.3.200.v20090526@default:default,org.eclipse.swt.win32.win32.x86_64*3.5.2.v3557f@default:false,org.eclipse.jem.util*2.0.201.v201001252130@default:default,org.eclipse.core.net*1.2.1.r35x_20090812-1200@default:default,org.eclipse.core.runtime.compatibility*3.2.0.v20090413@default:default,org.eclipse.ui.ide*3.5.2.M20100113-0800@default:default,org.eclipse.ecf*3.0.0.v20090831-1906@default:default,org.eclipse.text*3.5.0.v20090513-2000@default:default,ch.qos.logback.core@default:default,org.eclipse.equinox.security*1.0.100.v20090520-1800@default:default,org.eclipse.emf.edit*2.5.0.v200906151043@default:default,org.eclipse.equinox.p2.metadata*1.0.101.R35x_v20100112@default:default,javax.mail.glassfish@default:default,org.eclipse.debug.core*3.5.1.v20091103_r352@default:default,ch.qos.logback.classic@default:default,org.eclipse.ui.console*3.4.0.v20090513@default:default,org.apache.xml.serializer*2.7.1.v200902170519@default:default,org.eclipse.platform*3.3.202.v201002111343@default:default,org.eclipse.ui.forms*3.4.1.v20090714_35x@default:default,org.eclipse.search*3.5.1.r351_v20090708-0800@default:default,org.eclipse.ecf.provider.filetransfer*3.0.1.v20090831-1906@default:default,org.eclipse.equinox.p2.jarprocessor*1.0.100.v20090520-1905@default:default,org.apache.ant*1.7.1.v20090120-1145@default:default,org.eclipse.ecf.provider.filetransfer.ssl*1.0.0.v20090831-1906@default:false,org.eclipse.osgi.services*3.2.0.v20090520-1800@default:default,org.w3c.css.sac*1.3.1.v200903091627@default:default,org.mortbay.jetty.util*6.1.15.v200905182336@default:default,ch.qos.logback.slf4j@default:false,org.eclipse.ecf.identity*3.0.0.v20090831-1906@default:default,org.eclipse.equinox.p2.metadata.repository*1.0.101.R35x_v20090812@default:default,org.eclipse.ui.editors*3.5.0.v20090527-2000@default:default,org.eclipse.ecf.ssl*1.0.0.v20090831-1906@default:false,org.eclipse.core.filesystem*1.2.1.R35x_v20091203-1235@default:default,org.mortbay.jetty.server*6.1.15.v200905151201@default:default,org.eclipse.ui.workbench.texteditor*3.5.1.r352_v20100105@default:default,org.eclipse.update.core.win32*3.2.100.v20080107@default:false,org.eclipse.wst.common.project.facet.core*1.4.1.v200911141735@default:default,org.eclipse.compare*3.5.2.r35x_20100113-0800@default:default,org.eclipse.ui.views.properties.tabbed*3.5.0.I20090429-1800@default:default,org.eclipse.equinox.registry*3.4.100.v20090520-1800@default:default,org.eclipse.debug.ui*3.5.2.v20091028_r352@default:default,org.eclipse.help.base*3.4.0.v201002111343@default:default,org.eclipse.ui.intro.universal*3.2.300.v20090526@default:default,org.eclipse.wst.common.frameworks*1.1.300.v200904160730@default:default,org.eclipse.core.runtime*3.5.0.v20090525@default:true,org.eclipse.osgi.util*3.2.0.v20090520-1800@default:default,org.eclipse.core.runtime.compatibility.registry*3.2.200.v20090429-1800@default:false,org.eclipse.ecf.filetransfer*3.0.0.v20090831-1906@default:default,org.eclipse.equinox.http.servlet*1.0.200.v20090520-1800@default:default,org.eclipse.core.resources*3.5.2.R35x_v20091203-1235@default:default,org.apache.commons.logging*1.0.4.v200904062259@default:default,org.eclipse.wst.sse.core*1.1.402.v201001251516@default:default,org.apache.jasper*5.5.17.v200903231320@default:default,org.eclipse.wst.xml.ui*1.1.2.v201001222130@default:default,org.eclipse.core.databinding*1.2.0.M20090819-0800@default:default,org.eclipse.wst.validation*1.2.104.v200911120201@default:default,org.junit*3.8.2.v20090203-1005@default:default,org.apache.commons.el*1.0.0.v200806031608@default:default,org.eclipse.equinox.p2.repository*1.0.1.v20090901-1041@default:default,org.eclipse.ui.ide.application*1.0.101.M20090826-0800@default:default,org.eclipse.emf.ecore.edit*2.5.0.v200906151043@default:default,org.eclipse.help.ui*3.4.1.v20090819_35x@default:default,org.eclipse.equinox.p2.engine*1.0.102.R35x_v20091117@default:default,org.eclipse.core.runtime.compatibility.auth*3.2.100.v20090413@default:default,org.eclipse.wst.common.ui*1.1.402.v200901262305@default:default,org.eclipse.ui.workbench*3.5.2.M20100113-0800@default:default,org.eclipse.core.contenttype*3.4.1.R35x_v20090826-0451@default:default,org.apache.xml.resolver*1.2.0.v200902170519@default:default,javax.servlet.jsp@default:default,org.eclipse.core.resources.compatibility*3.4.1.R35x_v20100113-0530@default:false,org.eclipse.core.filesystem.win32.x86_64*1.1.0.v20090316-0910@default:false,org.eclipse.wst.sse.ui*1.1.102.v200910200227@default:default,org.eclipse.emf.ecore.xmi*2.5.0.v200906151043@default:default,org.apache.lucene.analysis*1.9.1.v20080530-1600@default:default,org.eclipse.swt*3.5.2.v3557f@default:default,org.eclipse.ui.win32*3.2.100.v20090429-1800@default:false,org.eclipse.wst.xml.core*1.1.402.v201001222130@default:default,org.eclipse.equinox.p2.artifact.repository*1.0.101.R35x_v20090721@default:default,org.eclipse.ui*3.5.2.M20100120-0800@default:default,org.eclipse.equinox.common*3.5.1.R35x_v20090807-1100@2:true,org.apache.lucene*1.9.1.v20080530-1600@default:default,org.eclipse.core.expressions*3.4.101.R35x_v20100209@default:default,org.eclipse.core.databinding.observable*1.2.0.M20090902-0800@default:default,org.eclipse.core.filebuffers*3.5.0.v20090526-2000@default:default"/>
-<stringAttribute key="selected_workspace_plugins" value="org.eclipse.vex.documentation@default:default,org.eclipse.vex.docbook@default:default,org.eclipse.ant.optional.junit@default:false,org.eclipse.vex.ui@default:default,org.eclipse.vex@default:default,org.eclipse.vex.dita@default:default,org.eclipse.vex.core@default:default"/>
-<booleanAttribute key="show_selected_only" value="false"/>
-<stringAttribute key="templateConfig" value="${target_home}\configuration\config.ini"/>
-<booleanAttribute key="tracing" value="false"/>
-<booleanAttribute key="useCustomFeatures" value="false"/>
-<booleanAttribute key="useDefaultConfig" value="true"/>
-<booleanAttribute key="useDefaultConfigArea" value="true"/>
-<booleanAttribute key="useProduct" value="true"/>
-<booleanAttribute key="usefeatures" value="false"/>
-</launchConfiguration>
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>

+<launchConfiguration type="org.eclipse.pde.ui.RuntimeWorkbench">

+<booleanAttribute key="append.args" value="true"/>

+<booleanAttribute key="askclear" value="true"/>

+<booleanAttribute key="automaticAdd" value="true"/>

+<booleanAttribute key="automaticValidate" value="false"/>

+<stringAttribute key="bootstrap" value=""/>

+<stringAttribute key="checked" value="[NONE]"/>

+<booleanAttribute key="clearConfig" value="true"/>

+<booleanAttribute key="clearws" value="false"/>

+<booleanAttribute key="clearwslog" value="false"/>

+<stringAttribute key="configLocation" value="${workspace_loc}/.metadata/.plugins/org.eclipse.pde.core/Vex"/>

+<booleanAttribute key="default" value="true"/>

+<stringAttribute key="deselected_workspace_plugins" value="org.eclipse.vex.core.tests,org.eclipse.vex.ui.tests,org.eclipse.vex.tests,org.eclipse.test,ft.vex.development"/>

+<booleanAttribute key="includeOptional" value="true"/>

+<stringAttribute key="location" value="${workspace_loc}/../runtime-vex"/>

+<stringAttribute key="org.eclipse.jdt.launching.JRE_CONTAINER" value="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6"/>

+<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-os ${target.os} -ws ${target.ws} -arch ${target.arch} -nl ${target.nl} -console"/>

+<stringAttribute key="org.eclipse.jdt.launching.SOURCE_PATH_PROVIDER" value="org.eclipse.pde.ui.workbenchClasspathProvider"/>

+<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-verbose:gc"/>

+<stringAttribute key="pde.version" value="3.3"/>

+<stringAttribute key="product" value="org.eclipse.platform.ide"/>

+<stringAttribute key="selected_target_plugins" value="org.eclipse.emf.ecore.change*2.5.0.v200906151043@default:default,org.eclipse.team.core*3.5.1.r35x_20100113-0800@default:default,org.eclipse.team.ui*3.5.0.I20090430-0408@default:default,org.eclipse.core.databinding.property*1.2.0.M20090819-0800@default:default,org.eclipse.update.configurator*3.3.0.v20090312@3:true,org.eclipse.wst.dtd.core*1.1.300.v200904181727@default:default,org.eclipse.ant.core*3.2.101.v20091110_r352@default:default,javax.xml*1.3.4.v201005080400@default:default,org.eclipse.osgi*3.5.2.R35x_v20100126@-1:true,org.eclipse.equinox.preferences*3.2.301.R35x_v20091117@default:default,org.eclipse.equinox.p2.core*1.0.101.R35x_v20090819@default:default,org.eclipse.wst.common.emfworkbench.integration*1.1.301.v200908101600@default:default,org.eclipse.wst.common.uriresolver*1.1.301.v200805140415@default:default,org.eclipse.compare.core*3.5.0.I20090430-0408@default:default,org.eclipse.ui.navigator*3.4.2.M20100120-0800@default:default,javax.activation*1.1.0.v201005080500@default:default,org.eclipse.update.ui*3.2.201.R35x_v20090813@default:default,javax.servlet*2.5.0.v200910301333@default:default,org.eclipse.jface*3.5.2.M20100120-0800@default:default,org.eclipse.ltk.ui.refactoring*3.4.101.r352_v20100209@default:default,org.eclipse.equinox.app*1.2.1.R35x_v20091203@default:default,org.eclipse.wst.common.core*1.1.201.v200806010600@default:default,org.eclipse.core.jobs*3.4.100.v20090429-1800@default:default,org.eclipse.help*3.4.1.v20090805_35x@default:default,org.eclipse.jface.text*3.5.2.r352_v20091118-0800@default:default,org.eclipse.core.variables*3.2.200.v20090521@default:default,org.eclipse.update.core*3.2.300.v20090525@default:default,org.eclipse.ltk.core.refactoring*3.5.0.v20090513-2000@default:default,org.eclipse.equinox.transforms.hook@default:false,org.eclipse.ui.intro*3.3.2.v20100111_35x@default:default,org.slf4j.api@default:default,org.eclipse.wst.common.environment*1.0.301.v200908101600@default:default,org.eclipse.equinox.concurrent*1.0.1.R35x_v20100209@default:default,org.eclipse.core.net.win32.x86_64*1.0.0.I20090306-1030@default:false,org.eclipse.ui.views*3.4.1.M20090826-0800@default:default,org.eclipse.emf.ecore*2.5.0.v200906151043@default:default,org.apache.xerces*2.9.0.v200909240008@default:default,org.eclipse.jface.databinding*1.3.1.M20090826-0800@default:default,org.eclipse.wst.common.emf*1.1.301.v200908181930@default:default,org.eclipse.core.commands*3.5.0.I20090525-2000@default:default,org.eclipse.ui.navigator.resources*3.4.1.M20090826-0800@default:default,com.ibm.icu*4.0.1.v20090822@default:default,org.eclipse.emf.common*2.5.0.v200906151043@default:default,org.eclipse.equinox.http.jetty*2.0.0.v20090520-1800@default:default,org.eclipse.ui.cheatsheets*3.3.200.v20090526@default:default,org.eclipse.swt.win32.win32.x86_64*3.5.2.v3557f@default:false,org.eclipse.jem.util*2.0.201.v201001252130@default:default,org.eclipse.core.net*1.2.1.r35x_20090812-1200@default:default,org.eclipse.core.runtime.compatibility*3.2.0.v20090413@default:default,org.eclipse.ui.ide*3.5.2.M20100113-0800@default:default,org.eclipse.ecf*3.0.0.v20090831-1906@default:default,org.eclipse.text*3.5.0.v20090513-2000@default:default,ch.qos.logback.core@default:default,org.eclipse.equinox.security*1.0.100.v20090520-1800@default:default,org.eclipse.emf.edit*2.5.0.v200906151043@default:default,org.eclipse.equinox.p2.metadata*1.0.101.R35x_v20100112@default:default,javax.mail.glassfish@default:default,org.eclipse.debug.core*3.5.1.v20091103_r352@default:default,ch.qos.logback.classic@default:default,org.eclipse.ui.console*3.4.0.v20090513@default:default,org.apache.xml.serializer*2.7.1.v200902170519@default:default,org.eclipse.platform*3.3.202.v201002111343@default:default,org.eclipse.ui.forms*3.4.1.v20090714_35x@default:default,org.eclipse.search*3.5.1.r351_v20090708-0800@default:default,org.eclipse.ecf.provider.filetransfer*3.0.1.v20090831-1906@default:default,org.eclipse.equinox.p2.jarprocessor*1.0.100.v20090520-1905@default:default,org.apache.ant*1.7.1.v20090120-1145@default:default,org.eclipse.ecf.provider.filetransfer.ssl*1.0.0.v20090831-1906@default:false,org.eclipse.osgi.services*3.2.0.v20090520-1800@default:default,org.w3c.css.sac*1.3.1.v200903091627@default:default,org.mortbay.jetty.util*6.1.15.v200905182336@default:default,ch.qos.logback.slf4j@default:false,org.eclipse.ecf.identity*3.0.0.v20090831-1906@default:default,org.eclipse.equinox.p2.metadata.repository*1.0.101.R35x_v20090812@default:default,org.eclipse.ui.editors*3.5.0.v20090527-2000@default:default,org.eclipse.ecf.ssl*1.0.0.v20090831-1906@default:false,org.eclipse.core.filesystem*1.2.1.R35x_v20091203-1235@default:default,org.mortbay.jetty.server*6.1.15.v200905151201@default:default,org.eclipse.ui.workbench.texteditor*3.5.1.r352_v20100105@default:default,org.eclipse.update.core.win32*3.2.100.v20080107@default:false,org.eclipse.wst.common.project.facet.core*1.4.1.v200911141735@default:default,org.eclipse.compare*3.5.2.r35x_20100113-0800@default:default,org.eclipse.ui.views.properties.tabbed*3.5.0.I20090429-1800@default:default,org.eclipse.equinox.registry*3.4.100.v20090520-1800@default:default,org.eclipse.debug.ui*3.5.2.v20091028_r352@default:default,org.eclipse.help.base*3.4.0.v201002111343@default:default,org.eclipse.ui.intro.universal*3.2.300.v20090526@default:default,org.eclipse.wst.common.frameworks*1.1.300.v200904160730@default:default,org.eclipse.core.runtime*3.5.0.v20090525@default:true,org.eclipse.osgi.util*3.2.0.v20090520-1800@default:default,org.eclipse.core.runtime.compatibility.registry*3.2.200.v20090429-1800@default:false,org.eclipse.ecf.filetransfer*3.0.0.v20090831-1906@default:default,org.eclipse.equinox.http.servlet*1.0.200.v20090520-1800@default:default,org.eclipse.core.resources*3.5.2.R35x_v20091203-1235@default:default,org.apache.commons.logging*1.0.4.v200904062259@default:default,org.eclipse.wst.sse.core*1.1.402.v201001251516@default:default,org.apache.jasper*5.5.17.v200903231320@default:default,org.eclipse.wst.xml.ui*1.1.2.v201001222130@default:default,org.eclipse.core.databinding*1.2.0.M20090819-0800@default:default,org.eclipse.wst.validation*1.2.104.v200911120201@default:default,org.junit*3.8.2.v20090203-1005@default:default,org.apache.commons.el*1.0.0.v200806031608@default:default,org.eclipse.equinox.p2.repository*1.0.1.v20090901-1041@default:default,org.eclipse.ui.ide.application*1.0.101.M20090826-0800@default:default,org.eclipse.emf.ecore.edit*2.5.0.v200906151043@default:default,org.eclipse.help.ui*3.4.1.v20090819_35x@default:default,org.eclipse.equinox.p2.engine*1.0.102.R35x_v20091117@default:default,org.eclipse.core.runtime.compatibility.auth*3.2.100.v20090413@default:default,org.eclipse.wst.common.ui*1.1.402.v200901262305@default:default,org.eclipse.ui.workbench*3.5.2.M20100113-0800@default:default,org.eclipse.core.contenttype*3.4.1.R35x_v20090826-0451@default:default,org.apache.xml.resolver*1.2.0.v200902170519@default:default,javax.servlet.jsp@default:default,org.eclipse.core.resources.compatibility*3.4.1.R35x_v20100113-0530@default:false,org.eclipse.core.filesystem.win32.x86_64*1.1.0.v20090316-0910@default:false,org.eclipse.wst.sse.ui*1.1.102.v200910200227@default:default,org.eclipse.emf.ecore.xmi*2.5.0.v200906151043@default:default,org.apache.lucene.analysis*1.9.1.v20080530-1600@default:default,org.eclipse.swt*3.5.2.v3557f@default:default,org.eclipse.ui.win32*3.2.100.v20090429-1800@default:false,org.eclipse.wst.xml.core*1.1.402.v201001222130@default:default,org.eclipse.equinox.p2.artifact.repository*1.0.101.R35x_v20090721@default:default,org.eclipse.ui*3.5.2.M20100120-0800@default:default,org.eclipse.equinox.common*3.5.1.R35x_v20090807-1100@2:true,org.apache.lucene*1.9.1.v20080530-1600@default:default,org.eclipse.core.expressions*3.4.101.R35x_v20100209@default:default,org.eclipse.core.databinding.observable*1.2.0.M20090902-0800@default:default,org.eclipse.core.filebuffers*3.5.0.v20090526-2000@default:default"/>

+<stringAttribute key="selected_workspace_plugins" value="org.eclipse.vex.documentation@default:default,org.eclipse.vex.docbook@default:default,org.eclipse.ant.optional.junit@default:false,org.eclipse.vex.ui@default:default,org.eclipse.vex@default:default,org.eclipse.vex.dita@default:default,org.eclipse.vex.core@default:default"/>

+<booleanAttribute key="show_selected_only" value="false"/>

+<stringAttribute key="templateConfig" value="${target_home}\configuration\config.ini"/>

+<booleanAttribute key="tracing" value="false"/>

+<booleanAttribute key="useCustomFeatures" value="false"/>

+<booleanAttribute key="useDefaultConfig" value="true"/>

+<booleanAttribute key="useDefaultConfigArea" value="true"/>

+<booleanAttribute key="useProduct" value="true"/>

+<booleanAttribute key="usefeatures" value="false"/>

+</launchConfiguration>

diff --git a/org.eclipse.vex.ui.tests/src/org/eclipse/vex/ui/internal/editor/tests/HandlerUtilTest.java b/org.eclipse.vex.ui.tests/src/org/eclipse/vex/ui/internal/editor/tests/HandlerUtilTest.java
index 06e215d..336594e 100644
--- a/org.eclipse.vex.ui.tests/src/org/eclipse/vex/ui/internal/editor/tests/HandlerUtilTest.java
+++ b/org.eclipse.vex.ui.tests/src/org/eclipse/vex/ui/internal/editor/tests/HandlerUtilTest.java
@@ -1,114 +1,116 @@
-/*******************************************************************************

- * Copyright (c) 2013 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.ui.internal.editor.tests;

-

-import static org.junit.Assert.assertEquals;

-import static org.junit.Assert.assertTrue;

-

-import java.net.URL;

-import java.util.List;

-

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

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

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

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

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

-import org.eclipse.vex.core.internal.widget.BaseVexWidget;

-import org.eclipse.vex.core.internal.widget.IVexWidget;

-import org.eclipse.vex.core.internal.widget.MockHostComponent;

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

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

-import org.eclipse.vex.ui.internal.handlers.VexHandlerUtil;

-import org.junit.Before;

-import org.junit.Test;

-

-public class HandlerUtilTest {

-

-	private IVexWidget widget;

-	private IElement table;

-

-	@Before

-	public void setUp() throws Exception {

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

-		final URL url = this.getClass().getResource("/tests/resources/tableTest.css");

-		final StyleSheet styleSheet = new StyleSheetReader().read(url);

-		widget.setDocument(new Document(new QualifiedName(null, "root")), styleSheet);

-		widget.insertElement(new QualifiedName(null, "root"));

-		table = widget.insertElement(new QualifiedName(null, "table"));

-	}

-

-	@Test

-	public void testGetCurrentTableRow() throws Exception {

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

-		widget.insertFragment(new XMLFragment("<tr><td>content</td><td>td2</td></tr>").getDocumentFragment());

-

-		final IElement currentRow = table.childElements().first();

-		assertEquals("tr", currentRow.getLocalName());

-		widget.moveTo(currentRow.children().first().getStartPosition().moveBy(1));

-		assertEquals(currentRow, VexHandlerUtil.getCurrentTableRow(widget));

-	}

-

-	@Test

-	public void testAddRowAbove() {

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

-		widget.insertFragment(new XMLFragment("<tr><td>content</td><td>td2</td></tr>").getDocumentFragment());

-

-		final IElement currentRow = table.childElements().first();

-		widget.moveTo(currentRow.children().first().getStartPosition().moveBy(1));

-		VexHandlerUtil.duplicateTableRow(widget, currentRow, true);

-		final List<? extends IElement> rows = table.childElements().asList();

-		assertEquals("Expecting two rows", 2, rows.size());

-		assertEquals("Expecting old row on 2nd position", currentRow, rows.get(1));

-		assertEquals("Expecting both cells to be created in new row", 2, rows.get(0).childElements().asList().size());

-	}

-

-	@Test

-	public void testAddRowBelow() {

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

-		widget.insertFragment(new XMLFragment("<tr><td>content</td><td>td2</td></tr>").getDocumentFragment());

-

-		final IElement currentRow = table.childElements().first();

-		widget.moveTo(currentRow.children().first().getStartPosition().moveBy(1));

-		VexHandlerUtil.duplicateTableRow(widget, currentRow, false);

-		final List<? extends IElement> rows = table.childElements().asList();

-		assertEquals("Expecting two rows", 2, rows.size());

-		assertEquals("Expecting old row on 1st position", currentRow, rows.get(0));

-		assertEquals("Expecting both cells to be created in new row", 2, rows.get(1).childElements().asList().size());

-	}

-

-	@Test

-	public void testDuplicateComments() {

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

-		widget.insertFragment(new XMLFragment("<tr><td>content</td><td>td2</td><!--comment--></tr>").getDocumentFragment());

-

-		final IElement currentRow = table.childElements().first();

-		widget.moveTo(currentRow.children().first().getStartPosition().moveBy(1));

-		VexHandlerUtil.duplicateTableRow(widget, currentRow, false);

-		final List<? extends IElement> rows = table.childElements().asList();

-		assertEquals("comment", rows.get(1).children().last().getText());

-	}

-

-	@Test

-	public void testDuplicateProcessingInstructions() {

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

-		widget.insertFragment(new XMLFragment("<tr><?target data?><td>content</td><td>td2</td></tr>").getDocumentFragment());

-

-		final IElement currentRow = table.childElements().first();

-		widget.moveTo(currentRow.children().first().getStartPosition().moveBy(1));

-		VexHandlerUtil.duplicateTableRow(widget, currentRow, false);

-		final List<? extends IElement> rows = table.childElements().asList();

-		assertTrue("Expecting a processing instruction", rows.get(1).children().withoutText().first() instanceof IProcessingInstruction);

-		assertEquals("Expecting all childs to be copied", 3, rows.get(1).children().withoutText().asList().size());

-		final IProcessingInstruction pi = (IProcessingInstruction) rows.get(1).children().first();

-		assertEquals("target", pi.getTarget());

-		assertEquals("data", pi.getText());

-	}

-}

+/*******************************************************************************
+ * Copyright (c) 2013 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.ui.internal.editor.tests;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.net.URL;
+import java.util.List;
+
+import org.eclipse.core.runtime.QualifiedName;
+import org.eclipse.vex.core.internal.css.StyleSheet;
+import org.eclipse.vex.core.internal.css.StyleSheetReader;
+import org.eclipse.vex.core.internal.dom.Document;
+import org.eclipse.vex.core.internal.io.XMLFragment;
+import org.eclipse.vex.core.internal.widget.BaseVexWidget;
+import org.eclipse.vex.core.internal.widget.CssTableModel;
+import org.eclipse.vex.core.internal.widget.IVexWidget;
+import org.eclipse.vex.core.internal.widget.MockHostComponent;
+import org.eclipse.vex.core.provisional.dom.IElement;
+import org.eclipse.vex.core.provisional.dom.IProcessingInstruction;
+import org.eclipse.vex.ui.internal.handlers.VexHandlerUtil;
+import org.junit.Before;
+import org.junit.Test;
+
+public class HandlerUtilTest {
+
+	private IVexWidget widget;
+	private IElement table;
+
+	@Before
+	public void setUp() throws Exception {
+		widget = new BaseVexWidget(new MockHostComponent());
+		final URL url = this.getClass().getResource("/tests/resources/tableTest.css");
+		final StyleSheet styleSheet = new StyleSheetReader().read(url);
+		widget.setTableModel(new CssTableModel(styleSheet));
+		widget.setDocument(new Document(new QualifiedName(null, "root")), styleSheet);
+		widget.insertElement(new QualifiedName(null, "root"));
+		table = widget.insertElement(new QualifiedName(null, "table"));
+	}
+
+	@Test
+	public void testGetCurrentTableRow() throws Exception {
+		widget.moveTo(table.getStartPosition().moveBy(1));
+		widget.insertFragment(new XMLFragment("<tr><td>content</td><td>td2</td></tr>").getDocumentFragment());
+
+		final IElement currentRow = table.childElements().first();
+		assertEquals("tr", currentRow.getLocalName());
+		widget.moveTo(currentRow.children().first().getStartPosition().moveBy(1));
+		assertEquals(currentRow, VexHandlerUtil.getCurrentTableRow(widget));
+	}
+
+	@Test
+	public void testAddRowAbove() {
+		widget.moveTo(table.getStartPosition().moveBy(1));
+		widget.insertFragment(new XMLFragment("<tr><td>content</td><td>td2</td></tr>").getDocumentFragment());
+
+		final IElement currentRow = table.childElements().first();
+		widget.moveTo(currentRow.children().first().getStartPosition().moveBy(1));
+		VexHandlerUtil.duplicateTableRow(widget, currentRow, true);
+		final List<? extends IElement> rows = table.childElements().asList();
+		assertEquals("Expecting two rows", 2, rows.size());
+		assertEquals("Expecting old row on 2nd position", currentRow, rows.get(1));
+		assertEquals("Expecting both cells to be created in new row", 2, rows.get(0).childElements().asList().size());
+	}
+
+	@Test
+	public void testAddRowBelow() {
+		widget.moveTo(table.getStartPosition().moveBy(1));
+		widget.insertFragment(new XMLFragment("<tr><td>content</td><td>td2</td></tr>").getDocumentFragment());
+
+		final IElement currentRow = table.childElements().first();
+		widget.moveTo(currentRow.children().first().getStartPosition().moveBy(1));
+		VexHandlerUtil.duplicateTableRow(widget, currentRow, false);
+		final List<? extends IElement> rows = table.childElements().asList();
+		assertEquals("Expecting two rows", 2, rows.size());
+		assertEquals("Expecting old row on 1st position", currentRow, rows.get(0));
+		assertEquals("Expecting both cells to be created in new row", 2, rows.get(1).childElements().asList().size());
+	}
+
+	@Test
+	public void testDuplicateComments() {
+		widget.moveTo(table.getStartPosition().moveBy(1));
+		widget.insertFragment(new XMLFragment("<tr><td>content</td><td>td2</td><!--comment--></tr>").getDocumentFragment());
+
+		final IElement currentRow = table.childElements().first();
+		widget.moveTo(currentRow.children().first().getStartPosition().moveBy(1));
+		VexHandlerUtil.duplicateTableRow(widget, currentRow, false);
+		final List<? extends IElement> rows = table.childElements().asList();
+		assertEquals("comment", rows.get(1).children().last().getText());
+	}
+
+	@Test
+	public void testDuplicateProcessingInstructions() {
+		widget.moveTo(table.getStartPosition().moveBy(1));
+		widget.insertFragment(new XMLFragment("<tr><?target data?><td>content</td><td>td2</td></tr>").getDocumentFragment());
+
+		final IElement currentRow = table.childElements().first();
+		widget.moveTo(currentRow.children().first().getStartPosition().moveBy(1));
+		VexHandlerUtil.duplicateTableRow(widget, currentRow, false);
+		final List<? extends IElement> rows = table.childElements().asList();
+		assertTrue("Expecting a processing instruction", rows.get(1).children().withoutText().first() instanceof IProcessingInstruction);
+		assertEquals("Expecting all childs to be copied", 3, rows.get(1).children().withoutText().asList().size());
+		final IProcessingInstruction pi = (IProcessingInstruction) rows.get(1).children().first();
+		assertEquals("target", pi.getTarget());
+		assertEquals("data", pi.getText());
+	}
+}
diff --git a/org.eclipse.vex.ui.tests/src/org/eclipse/vex/ui/internal/namespace/tests/EditNamespacesControllerTest.java b/org.eclipse.vex.ui.tests/src/org/eclipse/vex/ui/internal/namespace/tests/EditNamespacesControllerTest.java
index f8ddc95..d497a7b 100644
--- a/org.eclipse.vex.ui.tests/src/org/eclipse/vex/ui/internal/namespace/tests/EditNamespacesControllerTest.java
+++ b/org.eclipse.vex.ui.tests/src/org/eclipse/vex/ui/internal/namespace/tests/EditNamespacesControllerTest.java
@@ -1,173 +1,172 @@
-/*******************************************************************************

- * Copyright (c) 2011 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.ui.internal.namespace.tests;

-

-import static org.junit.Assert.assertEquals;

-import static org.junit.Assert.assertFalse;

-import static org.junit.Assert.assertNotNull;

-import static org.junit.Assert.assertNull;

-import static org.junit.Assert.assertSame;

-import static org.junit.Assert.assertTrue;

-import static org.junit.Assert.fail;

-

-import java.util.List;

-

-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.widget.BaseVexWidget;

-import org.eclipse.vex.core.internal.widget.IVexWidget;

-import org.eclipse.vex.core.internal.widget.MockHostComponent;

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

-import org.eclipse.vex.ui.internal.namespace.EditNamespacesController;

-import org.eclipse.vex.ui.internal.namespace.EditableNamespaceDefinition;

-import org.junit.Before;

-import org.junit.Test;

-

-/**

- * @author Florian Thienel

- */

-public class EditNamespacesControllerTest {

-

-	private IVexWidget widget;

-

-	@Before

-	public void setUp() throws Exception {

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

-		widget.setDocument(new Document(new QualifiedName(null, "root")), StyleSheet.NULL);

-	}

-

-	@Test

-	public void populateFromElement() throws Exception {

-		final IElement element = widget.insertElement(new QualifiedName("http://namespace/uri/default", "element"));

-		element.declareDefaultNamespace("http://namespace/uri/default");

-		element.declareNamespace("ns1", "http://namespace/uri/1");

-		element.declareNamespace("ns2", "http://namespace/uri/2");

-		final EditNamespacesController controller = new EditNamespacesController(widget);

-

-		assertEquals("http://namespace/uri/default", controller.getDefaultNamespaceURI());

-

-		final List<EditableNamespaceDefinition> namespaces = controller.getNamespaceDefinitions();

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

-		assertContainsNamespaceDefinition(new EditableNamespaceDefinition("ns1", "http://namespace/uri/1"), namespaces);

-		assertContainsNamespaceDefinition(new EditableNamespaceDefinition("ns2", "http://namespace/uri/2"), namespaces);

-	}

-

-	@Test

-	public void defaultNamespaceNotNull() throws Exception {

-		final EditNamespacesController controller = new EditNamespacesController(widget);

-		assertNotNull(controller.getDefaultNamespaceURI());

-	}

-

-	@Test

-	public void editDefaultNamespace() throws Exception {

-		final IElement element = widget.insertElement(new QualifiedName(null, "element"));

-		final EditNamespacesController controller = new EditNamespacesController(widget);

-		controller.setDefaultNamespaceURI("http://namespace/uri/default");

-		assertEquals("http://namespace/uri/default", controller.getDefaultNamespaceURI());

-		assertNull(element.getDeclaredDefaultNamespaceURI());

-

-		controller.applyToElement();

-		assertEquals("http://namespace/uri/default", element.getDefaultNamespaceURI());

-		assertNull(element.getQualifiedName().getQualifier());

-	}

-

-	@Test

-	public void removeDefaultNamespace() throws Exception {

-		final IElement element = widget.insertElement(new QualifiedName("http://namespace/uri/default", "element"));

-		element.declareDefaultNamespace("http://namespace/uri/default");

-		final EditNamespacesController controller = new EditNamespacesController(widget);

-

-		controller.setDefaultNamespaceURI("");

-		controller.applyToElement();

-		assertNull(element.getDeclaredDefaultNamespaceURI());

-		assertEquals("http://namespace/uri/default", element.getQualifiedName().getQualifier());

-	}

-

-	@Test

-	public void addNamespaceDefinition() throws Exception {

-		final IElement element = widget.insertElement(new QualifiedName(null, "element"));

-		final EditNamespacesController controller = new EditNamespacesController(widget);

-

-		assertTrue(controller.getNamespaceDefinitions().isEmpty());

-		assertTrue(element.getDeclaredNamespacePrefixes().isEmpty());

-

-		final EditableNamespaceDefinition newDefinition = controller.addNamespaceDefinition();

-		assertNotNull(newDefinition);

-

-		assertEquals(1, controller.getNamespaceDefinitions().size());

-		assertSame(newDefinition, controller.getNamespaceDefinitions().get(0));

-		assertTrue(element.getDeclaredNamespacePrefixes().isEmpty());

-

-		newDefinition.setPrefix("ns1");

-		newDefinition.setUri("http://namespace/uri/1");

-

-		controller.applyToElement();

-		assertEquals(1, element.getDeclaredNamespacePrefixes().size());

-	}

-

-	@Test

-	public void removeNamespaceDefinition() throws Exception {

-		final IElement element = widget.insertElement(new QualifiedName(null, "element"));

-		element.declareNamespace("ns1", "http://namespace/uri/1");

-		final EditNamespacesController controller = new EditNamespacesController(widget);

-

-		controller.removeNamespaceDefinition(controller.getNamespaceDefinitions().get(0));

-

-		assertTrue(controller.getNamespaceDefinitions().isEmpty());

-		assertTrue(element.getDeclaredNamespacePrefixes().contains("ns1"));

-

-		controller.applyToElement();

-		assertTrue(element.getDeclaredNamespacePrefixes().isEmpty());

-	}

-

-	@Test

-	public void editNamespacePrefix() throws Exception {

-		final IElement element = widget.insertElement(new QualifiedName(null, "element"));

-		element.declareNamespace("ns1", "http://namespace/uri/1");

-		final EditNamespacesController controller = new EditNamespacesController(widget);

-		final EditableNamespaceDefinition definition = controller.getNamespaceDefinitions().get(0);

-

-		definition.setPrefix("ns2");

-		assertTrue(element.getDeclaredNamespacePrefixes().contains("ns1"));

-		assertEquals(1, element.getDeclaredNamespacePrefixes().size());

-

-		controller.applyToElement();

-		assertFalse(element.getDeclaredNamespacePrefixes().contains("ns1"));

-		assertTrue(element.getDeclaredNamespacePrefixes().contains("ns2"));

-		assertEquals(1, element.getDeclaredNamespacePrefixes().size());

-	}

-

-	@Test

-	public void editNamespaceUri() throws Exception {

-		final IElement element = widget.insertElement(new QualifiedName(null, "element"));

-		element.declareNamespace("ns1", "http://namespace/uri/1");

-		final EditNamespacesController controller = new EditNamespacesController(widget);

-		final EditableNamespaceDefinition definition = controller.getNamespaceDefinitions().get(0);

-

-		definition.setUri("http://namespace/uri/2");

-		assertEquals("http://namespace/uri/1", element.getNamespaceURI("ns1"));

-

-		controller.applyToElement();

-		assertEquals("http://namespace/uri/2", element.getNamespaceURI("ns1"));

-		assertEquals(1, element.getDeclaredNamespacePrefixes().size());

-	}

-

-	private static void assertContainsNamespaceDefinition(final EditableNamespaceDefinition expected, final List<EditableNamespaceDefinition> actualList) {

-		for (final EditableNamespaceDefinition definition : actualList) {

-			if (expected.getPrefix().equals(definition.getPrefix()) && expected.getUri().equals(definition.getUri())) {

-				return;

-			}

-		}

-		fail("namespace definition not found: " + expected.getPrefix() + "=" + expected.getUri());

-	}

-

-}

+/*******************************************************************************
+ * Copyright (c) 2011 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.ui.internal.namespace.tests;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.List;
+
+import org.eclipse.core.runtime.QualifiedName;
+import org.eclipse.vex.core.internal.dom.Document;
+import org.eclipse.vex.core.internal.widget.BaseVexWidget;
+import org.eclipse.vex.core.internal.widget.IDocumentEditor;
+import org.eclipse.vex.core.internal.widget.MockHostComponent;
+import org.eclipse.vex.core.provisional.dom.IElement;
+import org.eclipse.vex.ui.internal.namespace.EditNamespacesController;
+import org.eclipse.vex.ui.internal.namespace.EditableNamespaceDefinition;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author Florian Thienel
+ */
+public class EditNamespacesControllerTest {
+
+	private IDocumentEditor editor;
+
+	@Before
+	public void setUp() throws Exception {
+		editor = new BaseVexWidget(new MockHostComponent());
+		editor.setDocument(new Document(new QualifiedName(null, "root")));
+	}
+
+	@Test
+	public void populateFromElement() throws Exception {
+		final IElement element = editor.insertElement(new QualifiedName("http://namespace/uri/default", "element"));
+		element.declareDefaultNamespace("http://namespace/uri/default");
+		element.declareNamespace("ns1", "http://namespace/uri/1");
+		element.declareNamespace("ns2", "http://namespace/uri/2");
+		final EditNamespacesController controller = new EditNamespacesController(editor);
+
+		assertEquals("http://namespace/uri/default", controller.getDefaultNamespaceURI());
+
+		final List<EditableNamespaceDefinition> namespaces = controller.getNamespaceDefinitions();
+		assertEquals(2, namespaces.size());
+		assertContainsNamespaceDefinition(new EditableNamespaceDefinition("ns1", "http://namespace/uri/1"), namespaces);
+		assertContainsNamespaceDefinition(new EditableNamespaceDefinition("ns2", "http://namespace/uri/2"), namespaces);
+	}
+
+	@Test
+	public void defaultNamespaceNotNull() throws Exception {
+		final EditNamespacesController controller = new EditNamespacesController(editor);
+		assertNotNull(controller.getDefaultNamespaceURI());
+	}
+
+	@Test
+	public void editDefaultNamespace() throws Exception {
+		final IElement element = editor.insertElement(new QualifiedName(null, "element"));
+		final EditNamespacesController controller = new EditNamespacesController(editor);
+		controller.setDefaultNamespaceURI("http://namespace/uri/default");
+		assertEquals("http://namespace/uri/default", controller.getDefaultNamespaceURI());
+		assertNull(element.getDeclaredDefaultNamespaceURI());
+
+		controller.applyToElement();
+		assertEquals("http://namespace/uri/default", element.getDefaultNamespaceURI());
+		assertNull(element.getQualifiedName().getQualifier());
+	}
+
+	@Test
+	public void removeDefaultNamespace() throws Exception {
+		final IElement element = editor.insertElement(new QualifiedName("http://namespace/uri/default", "element"));
+		element.declareDefaultNamespace("http://namespace/uri/default");
+		final EditNamespacesController controller = new EditNamespacesController(editor);
+
+		controller.setDefaultNamespaceURI("");
+		controller.applyToElement();
+		assertNull(element.getDeclaredDefaultNamespaceURI());
+		assertEquals("http://namespace/uri/default", element.getQualifiedName().getQualifier());
+	}
+
+	@Test
+	public void addNamespaceDefinition() throws Exception {
+		final IElement element = editor.insertElement(new QualifiedName(null, "element"));
+		final EditNamespacesController controller = new EditNamespacesController(editor);
+
+		assertTrue(controller.getNamespaceDefinitions().isEmpty());
+		assertTrue(element.getDeclaredNamespacePrefixes().isEmpty());
+
+		final EditableNamespaceDefinition newDefinition = controller.addNamespaceDefinition();
+		assertNotNull(newDefinition);
+
+		assertEquals(1, controller.getNamespaceDefinitions().size());
+		assertSame(newDefinition, controller.getNamespaceDefinitions().get(0));
+		assertTrue(element.getDeclaredNamespacePrefixes().isEmpty());
+
+		newDefinition.setPrefix("ns1");
+		newDefinition.setUri("http://namespace/uri/1");
+
+		controller.applyToElement();
+		assertEquals(1, element.getDeclaredNamespacePrefixes().size());
+	}
+
+	@Test
+	public void removeNamespaceDefinition() throws Exception {
+		final IElement element = editor.insertElement(new QualifiedName(null, "element"));
+		element.declareNamespace("ns1", "http://namespace/uri/1");
+		final EditNamespacesController controller = new EditNamespacesController(editor);
+
+		controller.removeNamespaceDefinition(controller.getNamespaceDefinitions().get(0));
+
+		assertTrue(controller.getNamespaceDefinitions().isEmpty());
+		assertTrue(element.getDeclaredNamespacePrefixes().contains("ns1"));
+
+		controller.applyToElement();
+		assertTrue(element.getDeclaredNamespacePrefixes().isEmpty());
+	}
+
+	@Test
+	public void editNamespacePrefix() throws Exception {
+		final IElement element = editor.insertElement(new QualifiedName(null, "element"));
+		element.declareNamespace("ns1", "http://namespace/uri/1");
+		final EditNamespacesController controller = new EditNamespacesController(editor);
+		final EditableNamespaceDefinition definition = controller.getNamespaceDefinitions().get(0);
+
+		definition.setPrefix("ns2");
+		assertTrue(element.getDeclaredNamespacePrefixes().contains("ns1"));
+		assertEquals(1, element.getDeclaredNamespacePrefixes().size());
+
+		controller.applyToElement();
+		assertFalse(element.getDeclaredNamespacePrefixes().contains("ns1"));
+		assertTrue(element.getDeclaredNamespacePrefixes().contains("ns2"));
+		assertEquals(1, element.getDeclaredNamespacePrefixes().size());
+	}
+
+	@Test
+	public void editNamespaceUri() throws Exception {
+		final IElement element = editor.insertElement(new QualifiedName(null, "element"));
+		element.declareNamespace("ns1", "http://namespace/uri/1");
+		final EditNamespacesController controller = new EditNamespacesController(editor);
+		final EditableNamespaceDefinition definition = controller.getNamespaceDefinitions().get(0);
+
+		definition.setUri("http://namespace/uri/2");
+		assertEquals("http://namespace/uri/1", element.getNamespaceURI("ns1"));
+
+		controller.applyToElement();
+		assertEquals("http://namespace/uri/2", element.getNamespaceURI("ns1"));
+		assertEquals(1, element.getDeclaredNamespacePrefixes().size());
+	}
+
+	private static void assertContainsNamespaceDefinition(final EditableNamespaceDefinition expected, final List<EditableNamespaceDefinition> actualList) {
+		for (final EditableNamespaceDefinition definition : actualList) {
+			if (expected.getPrefix().equals(definition.getPrefix()) && expected.getUri().equals(definition.getUri())) {
+				return;
+			}
+		}
+		fail("namespace definition not found: " + expected.getPrefix() + "=" + expected.getUri());
+	}
+
+}
diff --git a/org.eclipse.vex.ui/icons/anchor.gif b/org.eclipse.vex.ui/icons/anchor.gif
new file mode 100644
index 0000000..f58992d
--- /dev/null
+++ b/org.eclipse.vex.ui/icons/anchor.gif
Binary files differ
diff --git a/org.eclipse.vex.ui/icons/rebuild_structure.gif b/org.eclipse.vex.ui/icons/rebuild_structure.gif
new file mode 100644
index 0000000..00c7141
--- /dev/null
+++ b/org.eclipse.vex.ui/icons/rebuild_structure.gif
Binary files differ
diff --git a/org.eclipse.vex.ui/icons/refresh.gif b/org.eclipse.vex.ui/icons/refresh.gif
new file mode 100644
index 0000000..3ca04d0
--- /dev/null
+++ b/org.eclipse.vex.ui/icons/refresh.gif
Binary files differ
diff --git a/org.eclipse.vex.ui/icons/style_bold.gif b/org.eclipse.vex.ui/icons/style_bold.gif
new file mode 100644
index 0000000..643d9e6
--- /dev/null
+++ b/org.eclipse.vex.ui/icons/style_bold.gif
Binary files differ
diff --git a/org.eclipse.vex.ui/icons/style_italic.gif b/org.eclipse.vex.ui/icons/style_italic.gif
new file mode 100644
index 0000000..e309c22
--- /dev/null
+++ b/org.eclipse.vex.ui/icons/style_italic.gif
Binary files differ
diff --git a/org.eclipse.vex.ui/plugin.xml b/org.eclipse.vex.ui/plugin.xml
index 363cf22..1f9a3fd 100644
--- a/org.eclipse.vex.ui/plugin.xml
+++ b/org.eclipse.vex.ui/plugin.xml
@@ -189,6 +189,26 @@
             id="org.eclipse.vex.ui.SetProcessingInstructionTargetCommand"
             name="%command.setProcessingInstructionTarget.name">
       </command>
+      <command
+            categoryId="org.eclipse.vex.ui.commands.category"
+            id="org.eclipse.vex.ui.RebuildBoxModel"
+            name="Rebuild Box Model">
+      </command>
+      <command
+            categoryId="org.eclipse.vex.ui.commands.category"
+            id="org.eclipse.vex.ui.Bold"
+            name="Bold">
+      </command>
+      <command
+            categoryId="org.eclipse.vex.ui.commands.category"
+            id="org.eclipse.vex.ui.Italic"
+            name="Italic">
+      </command>
+      <command
+            categoryId="org.eclipse.vex.ui.commands.category"
+            id="org.eclipse.vex.ui.Anchor"
+            name="Anchor">
+      </command>
 
    </extension>
    <extension
@@ -774,6 +794,49 @@
             <reference definitionId="org.eclipse.vex.ui.activeVexEditor"/>
          </activeWhen>
       </handler>
+      <handler
+            class="org.eclipse.vex.ui.boxview.RefreshHandler"
+            commandId="org.eclipse.ui.file.refresh">
+         <activeWhen>
+            <reference
+                  definitionId="org.eclipse.vex.ui.activeBoxView">
+            </reference>
+         </activeWhen>
+      </handler>
+      <handler
+            class="org.eclipse.vex.ui.boxview.RebuildBoxModelHandler"
+            commandId="org.eclipse.vex.ui.RebuildBoxModel">
+      </handler>
+      <handler
+            class="org.eclipse.vex.ui.boxview.InsertBoldHandler"
+            commandId="org.eclipse.vex.ui.Bold">
+      </handler>
+      <handler
+            class="org.eclipse.vex.ui.boxview.InsertItalicHandler"
+            commandId="org.eclipse.vex.ui.Italic">
+      </handler>
+      <handler
+            class="org.eclipse.vex.ui.boxview.InsertAnchorHandler"
+            commandId="org.eclipse.vex.ui.Anchor">
+      </handler>
+      <handler
+            class="org.eclipse.vex.ui.boxview.InsertCommentHandler"
+            commandId="org.eclipse.vex.ui.AddCommentCommand">
+         <activeWhen>
+            <reference
+                  definitionId="org.eclipse.vex.ui.activeBoxView">
+            </reference>
+         </activeWhen>
+      </handler>
+      <handler
+            class="org.eclipse.vex.ui.boxview.InsertProcessingInstructionHandler"
+            commandId="org.eclipse.vex.ui.AddProcessingInstructionCommand">
+         <activeWhen>
+            <reference
+                  definitionId="org.eclipse.vex.ui.activeBoxView">
+            </reference>
+         </activeWhen>
+      </handler>
 
       <!-- TODO:
 		   - org.eclipse.ui.edit.text.join.lines
@@ -808,6 +871,12 @@
             id="org.eclipse.vex.ui.views.configuration"
             name="%ConfigurationView.name">
       </view>
+      <view
+            category="org.eclipse.vex.ui.views.VexViewCategory"
+            class="org.eclipse.vex.ui.boxview.BoxDemoView"
+            id="org.eclipse.vex.ui.views.box"
+            name="Vex Box Viewer">
+      </view>
    </extension>
    <extension
          id="pluginNature"
@@ -944,6 +1013,15 @@
             </with>
          </or>
       </definition>
+      <definition
+            id="org.eclipse.vex.ui.activeBoxView">
+         <with
+               variable="activePartId">
+            <equals
+                  value="org.eclipse.vex.ui.views.box">
+            </equals>
+         </with>
+      </definition>
    </extension>
    <extension
          point="org.eclipse.ui.services">
@@ -1254,6 +1332,53 @@
                visible="true">
          </separator>
       </menuContribution>
+      <menuContribution
+            allPopups="false"
+            locationURI="toolbar:org.eclipse.vex.ui.views.box">
+         <command
+               commandId="org.eclipse.ui.file.refresh"
+               icon="icons/refresh.gif"
+               label="Refresh"
+               style="push"
+               tooltip="Refresh the BoxView">
+         </command>
+         <command
+               commandId="org.eclipse.vex.ui.RebuildBoxModel"
+               icon="icons/rebuild_structure.gif"
+               label="Rebuild Box Model"
+               style="push">
+         </command>
+         <command
+               commandId="org.eclipse.vex.ui.Bold"
+               icon="icons/style_bold.gif"
+               label="Bold"
+               style="push">
+         </command>
+         <command
+               commandId="org.eclipse.vex.ui.Italic"
+               icon="icons/style_italic.gif"
+               label="Italic"
+               style="push">
+         </command>
+         <command
+               commandId="org.eclipse.vex.ui.Anchor"
+               icon="icons/anchor.gif"
+               label="Anchor"
+               style="push">
+         </command>
+         <command
+               commandId="org.eclipse.vex.ui.AddCommentCommand"
+               icon="icons/comment_obj.gif"
+               label="Comment"
+               style="push">
+         </command>
+         <command
+               commandId="org.eclipse.vex.ui.AddProcessingInstructionCommand"
+               icon="icons/proinst_obj.gif"
+               label="Processing Instruction"
+               style="push">
+         </command>
+      </menuContribution>
    </extension>
    <extension
          point="org.eclipse.ui.keywords">
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/boxview/BoxDemoView.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/boxview/BoxDemoView.java
new file mode 100644
index 0000000..4253f81
--- /dev/null
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/boxview/BoxDemoView.java
@@ -0,0 +1,263 @@
+/*******************************************************************************
+ * Copyright (c) 2014, 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.ui.boxview;
+
+import static org.eclipse.vex.core.internal.core.TextUtils.ANY_LINE_BREAKS;
+import static org.eclipse.vex.core.internal.core.TextUtils.CURRENCY_SIGN;
+import static org.eclipse.vex.core.internal.core.TextUtils.PARAGRAPH_SIGN;
+import static org.eclipse.vex.core.internal.core.TextUtils.RAQUO;
+import static org.eclipse.vex.core.internal.io.UniversalTestDocument.ANCHOR;
+import static org.eclipse.vex.core.internal.io.UniversalTestDocument.B;
+import static org.eclipse.vex.core.internal.io.UniversalTestDocument.I;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.net.URL;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IResourceChangeEvent;
+import org.eclipse.core.resources.IResourceChangeListener;
+import org.eclipse.core.resources.IResourceDelta;
+import org.eclipse.core.resources.IResourceDeltaVisitor;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.layout.RowData;
+import org.eclipse.swt.layout.RowLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.ui.IViewSite;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.part.ViewPart;
+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.io.UniversalTestDocument;
+import org.eclipse.vex.core.internal.visualization.CssBasedBoxModelBuilder;
+import org.eclipse.vex.core.internal.widget.CssTableModel;
+import org.eclipse.vex.core.internal.widget.swt.BoxWidget;
+import org.eclipse.vex.core.internal.widget.swt.IVexSelection;
+import org.eclipse.vex.core.provisional.dom.ContentRange;
+import org.eclipse.vex.core.provisional.dom.IContent;
+import org.eclipse.vex.core.provisional.dom.IDocument;
+import org.eclipse.vex.core.provisional.dom.IElement;
+import org.w3c.css.sac.CSSException;
+import org.w3c.css.sac.InputSource;
+
+/**
+ * This is a viewer for the new box model - just to do visual experiments.
+ *
+ * @author Florian Thienel
+ */
+public class BoxDemoView extends ViewPart {
+
+	private static final int CONTEXT_WINDOW = 5;
+	private static final int SAMPLE_COUNT = 25;
+	private static final IPath CSS_WORKSPACE_FILE = new Path("/test/box-demo.css");
+
+	private Composite boxWidgetParent;
+	private BoxWidget boxWidget;
+	private IDocument document;
+
+	private Label offsetLabel;
+	private Label contextLabel;
+
+	private final ISelectionChangedListener selectionChangedListener = new ISelectionChangedListener() {
+		@Override
+		public void selectionChanged(final SelectionChangedEvent event) {
+			updateInfoPanel(event.getSelection());
+		}
+	};
+
+	private final IResourceChangeListener resourceChangeListener = new IResourceChangeListener() {
+		@Override
+		public void resourceChanged(final IResourceChangeEvent event) {
+			try {
+				event.getDelta().accept(new IResourceDeltaVisitor() {
+					@Override
+					public boolean visit(final IResourceDelta delta) throws CoreException {
+						if (!delta.getFullPath().equals(CSS_WORKSPACE_FILE)) {
+							return true;
+						}
+						reloadStyleSheet();
+						return false;
+					}
+				});
+			} catch (final CoreException e) {
+				e.printStackTrace();
+			}
+		}
+	};
+
+	@Override
+	public void init(final IViewSite site) throws PartInitException {
+		super.init(site);
+		ResourcesPlugin.getWorkspace().addResourceChangeListener(resourceChangeListener, IResourceChangeEvent.POST_CHANGE);
+	}
+
+	@Override
+	public void createPartControl(final Composite parent) {
+		final Composite root = new Composite(parent, SWT.NONE);
+		root.setLayout(new GridLayout());
+
+		boxWidgetParent = new Composite(root, SWT.NONE);
+		boxWidgetParent.setLayoutData(new GridData(GridData.FILL, GridData.FILL, true, true));
+		boxWidgetParent.setLayout(new FillLayout());
+
+		final Composite infoPanel = new Composite(root, SWT.NONE);
+		infoPanel.setLayoutData(new GridData(GridData.FILL, GridData.FILL, true, false));
+		infoPanel.setLayout(new RowLayout());
+
+		new Label(infoPanel, SWT.NONE).setText("Caret Position:");
+		offsetLabel = new Label(infoPanel, SWT.LEFT);
+		offsetLabel.setLayoutData(new RowData(40, SWT.DEFAULT));
+
+		new Label(infoPanel, SWT.NONE).setText("Caret Context:");
+		contextLabel = new Label(infoPanel, SWT.LEFT);
+		contextLabel.setLayoutData(new RowData(SWT.DEFAULT, SWT.DEFAULT));
+
+		recreateBoxWidget();
+	}
+
+	@Override
+	public void dispose() {
+		super.dispose();
+		boxWidget = null;
+		ResourcesPlugin.getWorkspace().removeResourceChangeListener(resourceChangeListener);
+	}
+
+	@Override
+	public void setFocus() {
+		boxWidget.setFocus();
+	}
+
+	public void recreateBoxWidget() {
+		if (boxWidget != null) {
+			boxWidget.removeSelectionChangedListener(selectionChangedListener);
+			boxWidget.dispose();
+			boxWidget = null;
+			cleanStaleReferenceInShell();
+		}
+		boxWidget = new BoxWidget(boxWidgetParent, SWT.V_SCROLL);
+
+		document = UniversalTestDocument.createTestDocumentWithAllFeatures(SAMPLE_COUNT);
+		boxWidget.setDocument(document);
+		final StyleSheet styleSheet = readStyleSheet();
+		boxWidget.setBoxModelBuilder(new CssBasedBoxModelBuilder(styleSheet));
+		boxWidget.setWhitespacePolicy(new CssWhitespacePolicy(styleSheet));
+		boxWidget.setTableModel(new CssTableModel(styleSheet));
+		boxWidgetParent.layout();
+		boxWidget.addSelectionChangedListener(selectionChangedListener);
+
+		updateInfoPanel(boxWidget.getSelection());
+	}
+
+	private void cleanStaleReferenceInShell() {
+		/*
+		 * Shell keeps a reference to the boxWidget in Shell.savedFocus. parent.setFocus() forces Shell to store a
+		 * reference to parent instead.
+		 */
+		boxWidgetParent.setFocus();
+	}
+
+	private void updateInfoPanel(final ISelection selection) {
+		final int caretPosition = caretPosition(selection);
+		offsetLabel.setText(Integer.toString(caretPosition));
+		contextLabel.setText(caretContext(caretPosition, document.getContent()));
+		contextLabel.getParent().layout();
+	}
+
+	private static int caretPosition(final ISelection selection) {
+		return ((IVexSelection) selection).getCaretOffset();
+	}
+
+	private static String caretContext(final int caretPosition, final IContent content) {
+		final ContentRange contextRange = ContentRange.window(caretPosition, CONTEXT_WINDOW).limitTo(content.getRange());
+		final String rawContext = content.getRawText(contextRange);
+		final int caretIndexInText = caretPosition - contextRange.getStartOffset();
+
+		final String caretContext = (rawContext.substring(0, caretIndexInText) + "|" + rawContext.substring(caretIndexInText))
+				.replaceAll(ANY_LINE_BREAKS.pattern(), Character.toString(PARAGRAPH_SIGN))
+				.replaceAll("\0", Character.toString(CURRENCY_SIGN))
+				.replaceAll("\t", Character.toString(RAQUO));
+		return caretContext;
+	}
+
+	private void reloadStyleSheet() {
+		final StyleSheet styleSheet = readStyleSheet();
+		boxWidget.setBoxModelBuilder(new CssBasedBoxModelBuilder(styleSheet));
+		boxWidget.setWhitespacePolicy(new CssWhitespacePolicy(styleSheet));
+		rebuildBoxModel();
+	}
+
+	private StyleSheet readStyleSheet() {
+		try {
+			final URL styleSheetURL;
+			final InputStream styleSheetInputStream;
+			final IFile file = ResourcesPlugin.getWorkspace().getRoot().getFile(CSS_WORKSPACE_FILE);
+			if (file.exists()) {
+				styleSheetURL = file.getLocationURI().toURL();
+				styleSheetInputStream = file.getContents();
+			} else {
+				styleSheetURL = getClass().getResource("box-demo.css");
+				styleSheetInputStream = styleSheetURL.openStream();
+			}
+
+			final InputSource inputSource = new InputSource(new InputStreamReader(styleSheetInputStream, "utf-8"));
+			return new StyleSheetReader().read(inputSource, styleSheetURL);
+		} catch (final UnsupportedEncodingException e) {
+			e.printStackTrace();
+		} catch (final CSSException e) {
+			e.printStackTrace();
+		} catch (final IOException e) {
+			e.printStackTrace();
+		} catch (final CoreException e) {
+			e.printStackTrace();
+		}
+		return null;
+	}
+
+	public void rebuildBoxModel() {
+		boxWidget.refresh();
+	}
+
+	public void insertBold() {
+		boxWidget.insertElement(B);
+	}
+
+	public void insertItalic() {
+		boxWidget.insertElement(I);
+	}
+
+	public void insertAnchor() {
+		final IElement anchor = boxWidget.insertElement(ANCHOR);
+		anchor.setAttribute("id", Double.toHexString(Math.random()));
+	}
+
+	public void insertComment() {
+		boxWidget.insertComment();
+	}
+
+	public void insertProcessingInstruction(final String target) {
+		boxWidget.insertProcessingInstruction(target);
+	}
+
+}
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/boxview/InsertAnchorHandler.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/boxview/InsertAnchorHandler.java
new file mode 100644
index 0000000..2b6b605
--- /dev/null
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/boxview/InsertAnchorHandler.java
@@ -0,0 +1,32 @@
+/*******************************************************************************
+ * 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.ui.boxview;
+
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.core.expressions.IEvaluationContext;
+import org.eclipse.ui.ISources;
+
+/**
+ * @author Florian Thienel
+ */
+public class InsertAnchorHandler extends AbstractHandler {
+
+	@Override
+	public Object execute(final ExecutionEvent event) throws ExecutionException {
+		final BoxDemoView boxView = (BoxDemoView) ((IEvaluationContext) event.getApplicationContext()).getVariable(ISources.ACTIVE_PART_NAME);
+		boxView.insertAnchor();
+
+		return null;
+	}
+
+}
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/boxview/InsertBoldHandler.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/boxview/InsertBoldHandler.java
new file mode 100644
index 0000000..ee10e2f
--- /dev/null
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/boxview/InsertBoldHandler.java
@@ -0,0 +1,29 @@
+/*******************************************************************************
+ * 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.ui.boxview;
+
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.core.expressions.IEvaluationContext;
+import org.eclipse.ui.ISources;
+
+public class InsertBoldHandler extends AbstractHandler {
+
+	@Override
+	public Object execute(final ExecutionEvent event) throws ExecutionException {
+		final BoxDemoView boxView = (BoxDemoView) ((IEvaluationContext) event.getApplicationContext()).getVariable(ISources.ACTIVE_PART_NAME);
+		boxView.insertBold();
+
+		return null;
+	}
+
+}
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/boxview/InsertCommentHandler.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/boxview/InsertCommentHandler.java
new file mode 100644
index 0000000..e84f6a4
--- /dev/null
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/boxview/InsertCommentHandler.java
@@ -0,0 +1,32 @@
+/*******************************************************************************
+ * 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.ui.boxview;
+
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.core.expressions.IEvaluationContext;
+import org.eclipse.ui.ISources;
+
+/**
+ * @author Florian Thienel
+ */
+public class InsertCommentHandler extends AbstractHandler {
+
+	@Override
+	public Object execute(final ExecutionEvent event) throws ExecutionException {
+		final BoxDemoView boxView = (BoxDemoView) ((IEvaluationContext) event.getApplicationContext()).getVariable(ISources.ACTIVE_PART_NAME);
+		boxView.insertComment();
+
+		return null;
+	}
+
+}
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/boxview/InsertItalicHandler.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/boxview/InsertItalicHandler.java
new file mode 100644
index 0000000..d4dec9b
--- /dev/null
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/boxview/InsertItalicHandler.java
@@ -0,0 +1,29 @@
+/*******************************************************************************
+ * 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.ui.boxview;
+
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.core.expressions.IEvaluationContext;
+import org.eclipse.ui.ISources;
+
+public class InsertItalicHandler extends AbstractHandler {
+
+	@Override
+	public Object execute(final ExecutionEvent event) throws ExecutionException {
+		final BoxDemoView boxView = (BoxDemoView) ((IEvaluationContext) event.getApplicationContext()).getVariable(ISources.ACTIVE_PART_NAME);
+		boxView.insertItalic();
+
+		return null;
+	}
+
+}
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/boxview/InsertProcessingInstructionHandler.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/boxview/InsertProcessingInstructionHandler.java
new file mode 100644
index 0000000..cd84d24
--- /dev/null
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/boxview/InsertProcessingInstructionHandler.java
@@ -0,0 +1,32 @@
+/*******************************************************************************
+ * 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.ui.boxview;
+
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.core.expressions.IEvaluationContext;
+import org.eclipse.ui.ISources;
+
+/**
+ * @author Florian Thienel
+ */
+public class InsertProcessingInstructionHandler extends AbstractHandler {
+
+	@Override
+	public Object execute(final ExecutionEvent event) throws ExecutionException {
+		final BoxDemoView boxView = (BoxDemoView) ((IEvaluationContext) event.getApplicationContext()).getVariable(ISources.ACTIVE_PART_NAME);
+		boxView.insertProcessingInstruction("vex-demo");
+
+		return null;
+	}
+
+}
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/boxview/RebuildBoxModelHandler.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/boxview/RebuildBoxModelHandler.java
new file mode 100644
index 0000000..0601288
--- /dev/null
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/boxview/RebuildBoxModelHandler.java
@@ -0,0 +1,32 @@
+/*******************************************************************************
+ * 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.ui.boxview;
+
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.core.expressions.IEvaluationContext;
+import org.eclipse.ui.ISources;
+
+/**
+ * @author Florian Thienel
+ */
+public class RebuildBoxModelHandler extends AbstractHandler {
+
+	@Override
+	public Object execute(final ExecutionEvent event) throws ExecutionException {
+		final BoxDemoView boxView = (BoxDemoView) ((IEvaluationContext) event.getApplicationContext()).getVariable(ISources.ACTIVE_PART_NAME);
+		boxView.rebuildBoxModel();
+
+		return null;
+	}
+
+}
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/boxview/RefreshHandler.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/boxview/RefreshHandler.java
new file mode 100644
index 0000000..56f6f3e
--- /dev/null
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/boxview/RefreshHandler.java
@@ -0,0 +1,20 @@
+package org.eclipse.vex.ui.boxview;
+
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.core.expressions.IEvaluationContext;
+import org.eclipse.ui.ISources;
+
+public class RefreshHandler extends AbstractHandler {
+
+	@Override
+	public Object execute(final ExecutionEvent event) throws ExecutionException {
+		final BoxDemoView boxView = (BoxDemoView) ((IEvaluationContext) event.getApplicationContext()).getVariable(ISources.ACTIVE_PART_NAME);
+		boxView.recreateBoxWidget();
+		boxView.setFocus();
+		System.gc();
+
+		return null;
+	}
+}
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/boxview/box-demo.css b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/boxview/box-demo.css
new file mode 100644
index 0000000..e89ba4f
--- /dev/null
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/boxview/box-demo.css
@@ -0,0 +1,25 @@
+doc {
+	display: block;
+	padding: 3px 3px;
+	font: normal normal 12pt "Verdana";
+}
+
+section {
+	display: block;
+	padding: 3px 3px;
+}
+
+para {
+	display: block;
+	padding: 5px 4px;
+}
+
+b {
+	display: inline;
+	font-weight: bold;
+}
+
+i {
+	display: inline;
+	font-style: italic;
+}
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/editor/DocumentContextSourceProvider.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/editor/DocumentContextSourceProvider.java
index 4fd56ba..6f4f513 100644
--- a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/editor/DocumentContextSourceProvider.java
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/editor/DocumentContextSourceProvider.java
@@ -15,6 +15,8 @@
 
 import org.eclipse.ui.AbstractSourceProvider;
 import org.eclipse.ui.ISources;
+import org.eclipse.vex.core.internal.core.Rectangle;
+import org.eclipse.vex.core.internal.widget.IDocumentEditor;
 import org.eclipse.vex.core.internal.widget.swt.VexWidget;
 import org.eclipse.vex.core.provisional.dom.BaseNodeVisitorWithResult;
 import org.eclipse.vex.core.provisional.dom.IComment;
@@ -61,6 +63,10 @@
 	/** Variable ID of the <em>is-processing-instruction</em> flag. */
 	public static final String IS_PROCESSING_INSTRUCTION = "org.eclipse.vex.ui.isProcessingInstruction";
 
+	public static final String CARET_AREA = "org.eclipse.vex.ui.caretArea";
+
+	public static final String CURRENT_NODE = "org.eclipse.vex.ui.currentNode";
+
 	private boolean isColumn;
 	private boolean isFirstColumn;
 	private boolean isLastColumn;
@@ -71,6 +77,7 @@
 	private boolean isComment;
 	private boolean isProcessingInstruction;
 
+	private Rectangle caretArea;
 	private INode currentNode;
 
 	@Override
@@ -84,8 +91,8 @@
 	}
 
 	@Override
-	public Map<String, Boolean> getCurrentState() {
-		final Map<String, Boolean> currentState = new HashMap<String, Boolean>(6);
+	public Map<String, Object> getCurrentState() {
+		final Map<String, Object> currentState = new HashMap<String, Object>(20);
 		currentState.put(IS_COLUMN, Boolean.valueOf(isColumn));
 		currentState.put(IS_FIRST_COLUMN, Boolean.valueOf(isFirstColumn));
 		currentState.put(IS_LAST_COLUMN, Boolean.valueOf(isLastColumn));
@@ -95,21 +102,36 @@
 		currentState.put(IS_ELEMENT, Boolean.valueOf(isElement));
 		currentState.put(IS_COMMENT, Boolean.valueOf(isComment));
 		currentState.put(IS_PROCESSING_INSTRUCTION, Boolean.valueOf(isProcessingInstruction));
+		currentState.put(CARET_AREA, caretArea);
+		currentState.put(CURRENT_NODE, currentNode);
+
 		return currentState;
 	}
 
+	public void resetContext() {
+		currentNode = null;
+		caretArea = null;
+
+		final Map<String, Object> changes = new HashMap<String, Object>();
+		changes.put(CURRENT_NODE, currentNode);
+		changes.put(CARET_AREA, caretArea);
+		fireSourceChanged(ISources.WORKBENCH, changes);
+	}
+
 	/**
 	 * Synchronizes the variable values which will be exposed by this service with the specified {@link VexWidget}.
 	 *
-	 * @param widget
+	 * @param editor
 	 *            the Vex widget containing the actual states
+	 * @param caretArea
+	 *            TODO
 	 */
-	public void fireUpdate(final VexWidget widget) {
-		final Map<String, Boolean> changes = new HashMap<String, Boolean>();
-		final RowColumnInfo rowColumnInfo = VexHandlerUtil.getRowColumnInfo(widget);
+	public void fireUpdate(final IDocumentEditor editor, final Rectangle caretArea) {
+		final Map<String, Object> changes = new HashMap<String, Object>();
+		final RowColumnInfo rowColumnInfo = VexHandlerUtil.getRowColumnInfo(editor);
 
 		// column
-		final int columnIndex = VexHandlerUtil.getCurrentColumnIndex(widget);
+		final int columnIndex = VexHandlerUtil.getCurrentColumnIndex(editor);
 		final int columnCount = rowColumnInfo == null ? -1 : rowColumnInfo.maxColumnCount;
 		isColumn = update(changes, isColumn, columnIndex != -1, IS_COLUMN);
 		isFirstColumn = update(changes, isFirstColumn, columnIndex == 0, IS_FIRST_COLUMN);
@@ -123,11 +145,17 @@
 		isLastRow = update(changes, isLastRow, rowIndex == rowCount - 1, IS_LAST_ROW);
 
 		// nodes
-		final INode selectedNode = widget.getCurrentNode();
+		final INode selectedNode = editor.getCurrentNode();
 		if (!selectedNode.equals(currentNode)) {
 			// No need to evaluate if the node has not changed
 			currentNode = selectedNode;
 			changes.putAll(currentNode.accept(nodeTypeVisitor));
+			changes.put(CURRENT_NODE, currentNode);
+		}
+
+		if (!caretArea.equals(this.caretArea)) {
+			this.caretArea = caretArea;
+			changes.put(CARET_AREA, this.caretArea);
 		}
 
 		if (!changes.isEmpty()) {
@@ -135,7 +163,7 @@
 		}
 	}
 
-	private static boolean update(final Map<String, Boolean> changes, final boolean oldValue, final boolean newValue, final String valueName) {
+	private static boolean update(final Map<String, Object> changes, final boolean oldValue, final boolean newValue, final String valueName) {
 		if (newValue == oldValue) {
 			return oldValue;
 		}
@@ -144,10 +172,10 @@
 		return newValue;
 	}
 
-	private final INodeVisitorWithResult<Map<String, Boolean>> nodeTypeVisitor = new BaseNodeVisitorWithResult<Map<String, Boolean>>(new HashMap<String, Boolean>()) {
+	private final INodeVisitorWithResult<Map<String, Object>> nodeTypeVisitor = new BaseNodeVisitorWithResult<Map<String, Object>>(new HashMap<String, Object>()) {
 		@Override
-		public Map<String, Boolean> visit(final IElement element) {
-			final Map<String, Boolean> result = new HashMap<String, Boolean>(3);
+		public Map<String, Object> visit(final IElement element) {
+			final Map<String, Object> result = new HashMap<String, Object>(3);
 			result.put(IS_ELEMENT, true);
 			result.put(IS_COMMENT, false);
 			result.put(IS_PROCESSING_INSTRUCTION, false);
@@ -155,8 +183,8 @@
 		};
 
 		@Override
-		public Map<String, Boolean> visit(final IComment comment) {
-			final Map<String, Boolean> result = new HashMap<String, Boolean>(3);
+		public Map<String, Object> visit(final IComment comment) {
+			final Map<String, Object> result = new HashMap<String, Object>(3);
 			result.put(IS_ELEMENT, false);
 			result.put(IS_COMMENT, true);
 			result.put(IS_PROCESSING_INSTRUCTION, false);
@@ -164,8 +192,8 @@
 		};
 
 		@Override
-		public Map<String, Boolean> visit(final IProcessingInstruction pi) {
-			final Map<String, Boolean> result = new HashMap<String, Boolean>(3);
+		public Map<String, Object> visit(final IProcessingInstruction pi) {
+			final Map<String, Object> result = new HashMap<String, Object>(3);
 			result.put(IS_ELEMENT, false);
 			result.put(IS_COMMENT, false);
 			result.put(IS_PROCESSING_INSTRUCTION, true);
@@ -173,7 +201,7 @@
 		};
 
 		@Override
-		public Map<String, Boolean> visit(final IText text) {
+		public Map<String, Object> visit(final IText text) {
 			return text.getParent().accept(this);
 		}
 	};
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/editor/VexActionBarContributor.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/editor/VexActionBarContributor.java
index 58630bb..46e71f8 100644
--- a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/editor/VexActionBarContributor.java
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/editor/VexActionBarContributor.java
@@ -26,7 +26,7 @@
 import org.eclipse.ui.texteditor.FindReplaceAction;
 import org.eclipse.ui.texteditor.ITextEditorActionConstants;
 import org.eclipse.ui.texteditor.IWorkbenchActionDefinitionIds;
-import org.eclipse.vex.core.internal.widget.swt.VexWidget;
+import org.eclipse.vex.core.internal.widget.IDocumentEditor;
 import org.eclipse.vex.core.provisional.dom.DocumentValidationException;
 import org.eclipse.wst.xml.ui.internal.tabletree.XMLMultiPageEditorActionBarContributor;
 
@@ -45,7 +45,7 @@
 		return (VexEditor) activeEditor;
 	}
 
-	public VexWidget getVexWidget() {
+	public IDocumentEditor getDocumentEditor() {
 		if (activeEditor != null) {
 			return ((VexEditor) activeEditor).getVexWidget();
 		} else {
@@ -116,12 +116,12 @@
 	private final IAction undoAction = new UndoAction();
 
 	private void enableActions() {
-		final VexWidget widget = getVexWidget();
-		copyAction.setEnabled(widget != null && widget.hasSelection());
-		cutAction.setEnabled(widget != null && widget.hasSelection());
-		deleteAction.setEnabled(widget != null && widget.hasSelection());
-		redoAction.setEnabled(widget != null && widget.canRedo());
-		undoAction.setEnabled(widget != null && widget.canUndo());
+		final IDocumentEditor editor = getDocumentEditor();
+		copyAction.setEnabled(editor != null && editor.hasSelection());
+		cutAction.setEnabled(editor != null && editor.hasSelection());
+		deleteAction.setEnabled(editor != null && editor.hasSelection());
+		redoAction.setEnabled(editor != null && editor.canRedo());
+		undoAction.setEnabled(editor != null && editor.canUndo());
 	}
 
 	private final ISelectionListener selectionListener = new ISelectionListener() {
@@ -134,21 +134,21 @@
 	private class CopyAction extends Action {
 		@Override
 		public void run() {
-			getVexWidget().copySelection();
+			getDocumentEditor().copySelection();
 		}
 	};
 
 	private class CutAction extends Action {
 		@Override
 		public void run() {
-			getVexWidget().cutSelection();
+			getDocumentEditor().cutSelection();
 		}
 	}
 
 	private class DeleteAction extends Action {
 		@Override
 		public void run() {
-			getVexWidget().deleteSelection();
+			getDocumentEditor().deleteSelection();
 		}
 	};
 
@@ -156,7 +156,7 @@
 		@Override
 		public void run() {
 			try {
-				getVexWidget().paste();
+				getDocumentEditor().paste();
 			} catch (final DocumentValidationException e) {
 				// TODO Auto-generated catch block
 				e.printStackTrace();
@@ -167,15 +167,15 @@
 	private class SelectAllAction extends Action {
 		@Override
 		public void run() {
-			getVexWidget().selectAll();
+			getDocumentEditor().selectAll();
 		}
 	};
 
 	private class RedoAction extends Action {
 		@Override
 		public void run() {
-			if (getVexWidget().canRedo()) {
-				getVexWidget().redo();
+			if (getDocumentEditor().canRedo()) {
+				getDocumentEditor().redo();
 			}
 		}
 	};
@@ -183,8 +183,8 @@
 	private class UndoAction extends Action {
 		@Override
 		public void run() {
-			if (getVexWidget().canUndo()) {
-				getVexWidget().undo();
+			if (getDocumentEditor().canUndo()) {
+				getDocumentEditor().undo();
 			}
 		}
 	}
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/editor/VexEditor.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/editor/VexEditor.java
index 3760398..343fd12 100644
--- a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/editor/VexEditor.java
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/editor/VexEditor.java
@@ -48,10 +48,10 @@
 import org.eclipse.jface.text.DocumentEvent;
 import org.eclipse.jface.text.IFindReplaceTarget;
 import org.eclipse.jface.viewers.ISelectionChangedListener;
-import org.eclipse.jface.viewers.IStructuredSelection;
 import org.eclipse.jface.viewers.SelectionChangedEvent;
 import org.eclipse.jface.window.Window;
 import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Point;
 import org.eclipse.swt.layout.GridData;
 import org.eclipse.swt.layout.GridLayout;
 import org.eclipse.swt.widgets.Composite;
@@ -93,12 +93,17 @@
 import org.eclipse.ui.views.properties.IPropertySourceProvider;
 import org.eclipse.ui.views.properties.PropertySheetPage;
 import org.eclipse.vex.core.internal.core.ListenerList;
+import org.eclipse.vex.core.internal.core.Rectangle;
 import org.eclipse.vex.core.internal.css.CssWhitespacePolicy;
 import org.eclipse.vex.core.internal.dom.DocumentTextPosition;
 import org.eclipse.vex.core.internal.io.DocumentReader;
 import org.eclipse.vex.core.internal.io.DocumentWriter;
 import org.eclipse.vex.core.internal.validator.WTPVEXValidator;
-import org.eclipse.vex.core.internal.widget.swt.VexWidget;
+import org.eclipse.vex.core.internal.visualization.CssBasedBoxModelBuilder;
+import org.eclipse.vex.core.internal.widget.CssTableModel;
+import org.eclipse.vex.core.internal.widget.IDocumentEditor;
+import org.eclipse.vex.core.internal.widget.swt.BoxWidget;
+import org.eclipse.vex.core.internal.widget.swt.IVexSelection;
 import org.eclipse.vex.core.provisional.dom.AttributeChangeEvent;
 import org.eclipse.vex.core.provisional.dom.BaseNodeVisitorWithResult;
 import org.eclipse.vex.core.provisional.dom.ContentChangeEvent;
@@ -168,7 +173,7 @@
 	private IDocument document;
 	private Style style;
 
-	private VexWidget vexWidget;
+	private BoxWidget editorWidget;
 
 	private boolean dirty;
 
@@ -228,6 +233,14 @@
 			document.removeDocumentListener(documentListener);
 		}
 
+		if (style != null && document != null) {
+			style.getStyleSheet().flushAllStyles(document);
+		}
+
+		final ISourceProviderService service = (ISourceProviderService) getEditorSite().getWorkbenchWindow().getService(ISourceProviderService.class);
+		final DocumentContextSourceProvider contextProvider = (DocumentContextSourceProvider) service.getSourceProvider(DocumentContextSourceProvider.IS_COLUMN);
+		contextProvider.resetContext();
+
 		getEditorSite().getSelectionProvider().removeSelectionChangedListener(selectionChangedListener);
 
 		if (parentControl != null) {
@@ -463,14 +476,17 @@
 				jFaceDoc.removePosition(positionOfCurrentNode);
 			}
 
-			positionOfCurrentNode = createDocumentWriter().write(document, doc, vexWidget.getCurrentNode());
-			positionOfCurrentNode.setOffsetInNode(vexWidget.getCaretPosition().getOffset() - vexWidget.getCurrentNode().getStartPosition().getOffset());
+			final INode currentNode = editorWidget.getCurrentNode();
+			if (currentNode != null) {
+				positionOfCurrentNode = createDocumentWriter().write(document, doc, currentNode);
+				positionOfCurrentNode.setOffsetInNode(editorWidget.getCaretPosition().getOffset() - currentNode.getStartPosition().getOffset());
 
-			try {
-				jFaceDoc.addPosition(positionOfCurrentNode);
-			} catch (final BadLocationException e) {
-				// That should not happen
-				e.printStackTrace();
+				try {
+					jFaceDoc.addPosition(positionOfCurrentNode);
+				} catch (final BadLocationException e) {
+					// That should not happen
+					e.printStackTrace();
+				}
 			}
 		} finally {
 			provider.changed(getEditorInput());
@@ -481,7 +497,7 @@
 
 	private DocumentWriter createDocumentWriter() {
 		final DocumentWriter result = new DocumentWriter();
-		result.setWhitespacePolicy(vexWidget.getWhitespacePolicy());
+		result.setWhitespacePolicy(editorWidget.getWhitespacePolicy());
 		result.setIndent(preferences.getIndentationPattern());
 		result.setWrapColumn(preferences.getLineWidth());
 		return result;
@@ -524,8 +540,8 @@
 	/**
 	 * Returns the VexWidget that implements this editor.
 	 */
-	public VexWidget getVexWidget() {
-		return vexWidget;
+	public IDocumentEditor getVexWidget() {
+		return editorWidget;
 	}
 
 	public void gotoMarker(final IMarker marker) {
@@ -645,8 +661,8 @@
 
 	@Override
 	public void setFocus() {
-		if (vexWidget != null) {
-			vexWidget.setFocus();
+		if (editorWidget != null) {
+			editorWidget.setFocus();
 			setStatus(getLocationPath());
 		}
 	}
@@ -700,7 +716,7 @@
 		VexDocumentContentModel documentContentModel;
 
 		try {
-			if (vexWidget != null) {
+			if (editorWidget != null) {
 				vexEditorListeners.fireEvent("documentUnloaded", new VexEditorEvent(this)); //$NON-NLS-1$
 			}
 			if (document != null) {
@@ -797,15 +813,16 @@
 
 			document.addDocumentListener(documentListener);
 
-			vexWidget.setDebugging(debugging);
-			vexWidget.setWhitespacePolicy(reader.getWhitespacePolicy());
-			vexWidget.setDocument(document, style.getStyleSheet());
-			vexWidget.setReadOnly(isEditorInputReadOnly());
+			editorWidget.setBoxModelBuilder(new CssBasedBoxModelBuilder(style.getStyleSheet()));
+			editorWidget.setWhitespacePolicy(reader.getWhitespacePolicy());
+			editorWidget.setTableModel(new CssTableModel(style.getStyleSheet()));
+			editorWidget.setDocument(document);
+			editorWidget.setReadOnly(isEditorInputReadOnly());
 
 			final INode nodeAtCaret = reader.getNodeAtCaret();
 			if (nodeAtCaret != null) {
 				final int offsetInNode = Math.min(nodeAtCaret.getStartOffset() + positionOfCurrentNode.getOffsetInNode(), nodeAtCaret.getEndOffset());
-				vexWidget.moveTo(new ContentPosition(document, offsetInNode));
+				editorWidget.moveTo(new ContentPosition(document, offsetInNode));
 			}
 
 			loaded = true;
@@ -867,8 +884,10 @@
 	 */
 	public void setStyle(final Style style) {
 		this.style = style;
-		if (vexWidget != null) {
-			vexWidget.setStyleSheet(style.getStyleSheet());
+		if (editorWidget != null) {
+			editorWidget.setBoxModelBuilder(new CssBasedBoxModelBuilder(style.getStyleSheet()));
+			editorWidget.setWhitespacePolicy(new CssWhitespacePolicy(style.getStyleSheet()));
+			editorWidget.setTableModel(new CssTableModel(style.getStyleSheet()));
 			preferences.setPreferredStyleId(doctype, style.getUniqueId());
 		}
 		vexEditorListeners.fireEvent("styleChanged", new VexEditorEvent(this)); //$NON-NLS-1$
@@ -884,9 +903,9 @@
 	 */
 	private void showLabel(final String message, final Exception ex) {
 		if (loadingLabel == null) {
-			if (vexWidget != null) {
-				vexWidget.dispose();
-				vexWidget = null;
+			if (editorWidget != null) {
+				editorWidget.dispose();
+				editorWidget = null;
 			}
 			final GridLayout layout = new GridLayout();
 			layout.numColumns = 1;
@@ -926,7 +945,7 @@
 
 	private void showVexWidget() {
 
-		if (vexWidget != null) {
+		if (editorWidget != null) {
 			return;
 		}
 
@@ -964,17 +983,17 @@
 		gd.horizontalAlignment = GridData.FILL;
 		gd.verticalAlignment = GridData.FILL;
 
-		vexWidget = new VexWidget(parentControl, SWT.V_SCROLL);
+		editorWidget = new BoxWidget(parentControl, SWT.V_SCROLL);
 		gd = new GridData();
 		gd.grabExcessHorizontalSpace = true;
 		gd.grabExcessVerticalSpace = true;
 		gd.horizontalAlignment = GridData.FILL;
 		gd.verticalAlignment = GridData.FILL;
-		vexWidget.setLayoutData(gd);
+		editorWidget.setLayoutData(gd);
 
 		final MenuManager menuManager = new MenuManager();
-		getSite().registerContextMenu("org.eclipse.vex.ui.popup", menuManager, vexWidget);
-		vexWidget.setMenu(menuManager.createContextMenu(vexWidget));
+		getSite().registerContextMenu("org.eclipse.vex.ui.popup", menuManager, editorWidget);
+		editorWidget.setMenu(menuManager.createContextMenu(editorWidget));
 
 		setClean();
 
@@ -982,7 +1001,7 @@
 		final IContextService cs = (IContextService) getSite().getService(IContextService.class);
 		cs.activateContext("org.eclipse.vex.ui.VexEditorContext");
 
-		vexWidget.addSelectionChangedListener(selectionProvider);
+		editorWidget.addSelectionChangedListener(selectionProvider);
 
 		parentControl.layout(true);
 
@@ -1005,7 +1024,9 @@
 						// Oops, style went bye-bye
 						// Let's just hold on to it in case it comes back later
 					} else {
-						vexWidget.setStyleSheet(newStyle.getStyleSheet());
+						editorWidget.setBoxModelBuilder(new CssBasedBoxModelBuilder(style.getStyleSheet()));
+						editorWidget.setWhitespacePolicy(new CssWhitespacePolicy(style.getStyleSheet()));
+						editorWidget.setTableModel(new CssTableModel(style.getStyleSheet()));
 						style = newStyle;
 					}
 				}
@@ -1041,10 +1062,16 @@
 			// update context service
 			final ISourceProviderService service = (ISourceProviderService) window.getService(ISourceProviderService.class);
 			final DocumentContextSourceProvider contextProvider = (DocumentContextSourceProvider) service.getSourceProvider(DocumentContextSourceProvider.IS_COLUMN);
-			contextProvider.fireUpdate(vexWidget);
+			contextProvider.fireUpdate(editorWidget, getCaretArea());
 		}
 	};
 
+	private Rectangle getCaretArea() {
+		final Rectangle relativeArea = editorWidget.getCaretArea();
+		final Point caretLocation = editorWidget.toDisplay(new Point(relativeArea.getX(), relativeArea.getY()));
+		return new Rectangle(caretLocation.x, caretLocation.y, relativeArea.getWidth(), relativeArea.getHeight());
+	}
+
 	private final IDocumentListener documentListener = new IDocumentListener() {
 
 		@Override
@@ -1108,7 +1135,7 @@
 
 	private String getLocationPath() {
 		final List<String> path = new ArrayList<String>();
-		INode node = vexWidget.getCurrentNode();
+		INode node = editorWidget.getCurrentNode();
 		while (node != null) {
 			path.add(node.accept(nodePathVisitor));
 			node = node.getParent();
@@ -1139,15 +1166,15 @@
 				@Override
 				public IPropertySource getPropertySource(final Object object) {
 					if (object instanceof IElement) {
-						final IStructuredSelection selection = (IStructuredSelection) vexWidget.getSelection();
+						final IVexSelection selection = editorWidget.getSelection();
 						final boolean multipleElementsSelected = selection != null && selection.size() > 1;
-						final IValidator validator = vexWidget.getDocument().getValidator();
+						final IValidator validator = editorWidget.getDocument().getValidator();
 						return new ElementPropertySource((IElement) object, validator, multipleElementsSelected);
 					}
 					if (object instanceof IIncludeNode) {
-						final IStructuredSelection selection = (IStructuredSelection) vexWidget.getSelection();
+						final IVexSelection selection = editorWidget.getSelection();
 						final boolean multipleElementsSelected = selection != null && selection.size() > 1;
-						final IValidator validator = vexWidget.getDocument().getValidator();
+						final IValidator validator = editorWidget.getDocument().getValidator();
 						return new ElementPropertySource(((IIncludeNode) object).getReference(), validator, multipleElementsSelected);
 					}
 					if (object instanceof IDocument) {
@@ -1183,16 +1210,16 @@
 
 				@Override
 				protected void inDocumentReplaceSelection(final CharSequence text) {
-					final VexWidget vexWidget = getVexWidget();
+					final IDocumentEditor documentEditor = getVexWidget();
 
 					// because of Undo this action must be atomic
-					vexWidget.beginWork();
-					try {
-						vexWidget.deleteSelection();
-						vexWidget.insertText(text.toString());
-					} finally {
-						vexWidget.endWork(true);
-					}
+					documentEditor.doWork(new Runnable() {
+						@Override
+						public void run() {
+							documentEditor.deleteSelection();
+							documentEditor.insertText(text.toString());
+						}
+					});
 				}
 
 			};
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AbstractAddColumnHandler.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AbstractAddColumnHandler.java
index 115921c..469eb72 100644
--- a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AbstractAddColumnHandler.java
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AbstractAddColumnHandler.java
@@ -19,7 +19,7 @@
 import org.eclipse.core.commands.ExecutionException;
 import org.eclipse.vex.core.internal.dom.CopyOfElement;
 import org.eclipse.vex.core.internal.layout.LayoutUtils.ElementOrRange;
-import org.eclipse.vex.core.internal.widget.swt.VexWidget;
+import org.eclipse.vex.core.internal.widget.IDocumentEditor;
 import org.eclipse.vex.core.provisional.dom.ContentPosition;
 import org.eclipse.vex.core.provisional.dom.IElement;
 
@@ -33,12 +33,12 @@
 
 	@Override
 	public Object execute(final ExecutionEvent event) throws ExecutionException {
-		final VexWidget widget = VexHandlerUtil.computeWidget(event);
-		widget.doWork(new Runnable() {
+		final IDocumentEditor editor = VexHandlerUtil.getDocumentEditor(event);
+		editor.doWork(new Runnable() {
 			@Override
 			public void run() {
 				try {
-					addColumn(widget);
+					addColumn(editor);
 				} catch (final ExecutionException e) {
 					throw new RuntimeException(e);
 				}
@@ -47,8 +47,8 @@
 		return null;
 	}
 
-	private void addColumn(final VexWidget widget) throws ExecutionException {
-		final int indexToDup = VexHandlerUtil.getCurrentColumnIndex(widget);
+	private void addColumn(final IDocumentEditor editor) throws ExecutionException {
+		final int indexToDup = VexHandlerUtil.getCurrentColumnIndex(editor);
 
 		// adding possible?
 		if (indexToDup == -1) {
@@ -56,7 +56,7 @@
 		}
 
 		final List<IElement> cellsToDup = new ArrayList<IElement>();
-		VexHandlerUtil.iterateTableCells(widget, new TableCellCallbackAdapter() {
+		VexHandlerUtil.iterateTableCells(editor, new TableCellCallbackAdapter() {
 			@Override
 			public void onCell(final ElementOrRange row, final ElementOrRange cell, final int rowIndex, final int cellIndex) {
 				if (cellIndex == indexToDup && cell instanceof IElement) {
@@ -66,14 +66,14 @@
 		});
 
 		for (final IElement element : cellsToDup) {
-			widget.moveTo(addBefore() ? element.getStartPosition() : element.getEndPosition().moveBy(1));
-			widget.insertElement(element.getQualifiedName()).accept(new CopyOfElement(element));
+			editor.moveTo(addBefore() ? element.getStartPosition() : element.getEndPosition().moveBy(1));
+			editor.insertElement(element.getQualifiedName()).accept(new CopyOfElement(element));
 		}
 
 		// Place caret in first new cell
 		final IElement firstCell = cellsToDup.get(0);
 		final ContentPosition position = new ContentPosition(firstCell, firstCell.getStartOffset() + 1);
-		widget.moveTo(position);
+		editor.moveTo(position);
 	}
 
 	/**
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AbstractAddRowHandler.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AbstractAddRowHandler.java
index b57204e..93d2a6a 100644
--- a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AbstractAddRowHandler.java
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AbstractAddRowHandler.java
@@ -12,8 +12,9 @@
  *******************************************************************************/
 package org.eclipse.vex.ui.internal.handlers;
 
+import org.eclipse.core.commands.ExecutionEvent;
 import org.eclipse.core.commands.ExecutionException;
-import org.eclipse.vex.core.internal.widget.swt.VexWidget;
+import org.eclipse.vex.core.internal.widget.IDocumentEditor;
 import org.eclipse.vex.core.provisional.dom.IElement;
 
 /**
@@ -26,11 +27,11 @@
 public abstract class AbstractAddRowHandler extends AbstractVexWidgetHandler {
 
 	@Override
-	public void execute(final VexWidget widget) throws ExecutionException {
-		widget.doWork(new Runnable() {
+	public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+		editor.doWork(new Runnable() {
 			@Override
 			public void run() {
-				addRow(widget);
+				addRow(editor);
 			}
 		});
 	}
@@ -40,16 +41,16 @@
 	 */
 	protected abstract boolean addAbove();
 
-	private void addRow(final VexWidget widget) {
+	private void addRow(final IDocumentEditor editor) {
 		// Find the parent table row
-		final IElement currentRow = VexHandlerUtil.getCurrentTableRow(widget);
+		final IElement currentRow = VexHandlerUtil.getCurrentTableRow(editor);
 
 		// Do nothing is the caret is not inside a table row
-		if (currentRow == widget.getDocument().getRootElement()) {
+		if (currentRow == editor.getDocument().getRootElement()) {
 			return;
 		}
 
-		VexHandlerUtil.duplicateTableRow(widget, currentRow, addAbove());
+		VexHandlerUtil.duplicateTableRow(editor, currentRow, addAbove());
 
 	}
 }
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AbstractMoveColumnHandler.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AbstractMoveColumnHandler.java
index 3644239..3140b00 100644
--- a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AbstractMoveColumnHandler.java
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AbstractMoveColumnHandler.java
@@ -17,7 +17,7 @@
 import org.eclipse.core.commands.ExecutionEvent;
 import org.eclipse.core.commands.ExecutionException;
 import org.eclipse.vex.core.internal.layout.LayoutUtils.ElementOrRange;
-import org.eclipse.vex.core.internal.widget.swt.VexWidget;
+import org.eclipse.vex.core.internal.widget.IDocumentEditor;
 import org.eclipse.vex.core.provisional.dom.ContentPositionRange;
 
 /**
@@ -30,20 +30,20 @@
 
 	@Override
 	public Object execute(final ExecutionEvent event) throws ExecutionException {
-		final VexWidget widget = VexHandlerUtil.computeWidget(event);
-		final VexHandlerUtil.RowColumnInfo rcInfo = VexHandlerUtil.getRowColumnInfo(widget);
+		final IDocumentEditor editor = VexHandlerUtil.getDocumentEditor(event);
+		final VexHandlerUtil.RowColumnInfo rcInfo = VexHandlerUtil.getRowColumnInfo(editor);
 
 		if (rcInfo == null || !movingPossible(rcInfo)) {
 			return null;
 		}
 
-		widget.doWork(new Runnable() {
+		editor.doWork(new Runnable() {
 			@Override
 			public void run() {
 				final List<Object> sourceCells = new ArrayList<Object>();
 				final List<Object> targetCells = new ArrayList<Object>();
-				computeCells(widget, rcInfo, sourceCells, targetCells);
-				swapCells(widget, sourceCells, targetCells);
+				computeCells(editor, rcInfo, sourceCells, targetCells);
+				swapCells(editor, sourceCells, targetCells);
 			}
 		}, true);
 		return null;
@@ -54,9 +54,9 @@
 	 */
 	protected abstract boolean moveRight();
 
-	private void computeCells(final VexWidget widget, final VexHandlerUtil.RowColumnInfo rcInfo, final List<Object> sourceCells, final List<Object> targetCells) {
+	private void computeCells(final IDocumentEditor editor, final VexHandlerUtil.RowColumnInfo rcInfo, final List<Object> sourceCells, final List<Object> targetCells) {
 
-		VexHandlerUtil.iterateTableCells(widget, new TableCellCallbackAdapter() {
+		VexHandlerUtil.iterateTableCells(editor, new TableCellCallbackAdapter() {
 
 			private Object leftCell;
 
@@ -73,7 +73,7 @@
 		});
 	}
 
-	private void swapCells(final VexWidget widget, final List<Object> sourceCells, final List<Object> targetCells) {
+	private void swapCells(final IDocumentEditor editor, final List<Object> sourceCells, final List<Object> targetCells) {
 
 		// Iterate the deletions in reverse, so that we don't mess up offsets
 		// that are in anonymous cells, which are not stored as positions.
@@ -86,16 +86,16 @@
 			final Object target = targetCells.get(i);
 			final ContentPositionRange sourceRange = VexHandlerUtil.getOuterRange(source);
 			final ContentPositionRange targetRange = VexHandlerUtil.getOuterRange(target);
-			widget.moveTo(moveRight() ? targetRange.getStartPosition() : targetRange.getEndPosition());
-			widget.savePosition(new Runnable() {
+			editor.moveTo(moveRight() ? targetRange.getStartPosition() : targetRange.getEndPosition());
+			editor.savePosition(new Runnable() {
 				@Override
 				public void run() {
-					widget.moveTo(sourceRange.getStartPosition());
-					widget.moveTo(sourceRange.getEndPosition(), true);
-					widget.cutSelection();
+					editor.moveTo(sourceRange.getStartPosition());
+					editor.moveTo(sourceRange.getEndPosition(), true);
+					editor.cutSelection();
 				}
 			});
-			widget.paste();
+			editor.paste();
 
 		}
 	}
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AbstractMoveRowHandler.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AbstractMoveRowHandler.java
index 609c867..3b14921 100644
--- a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AbstractMoveRowHandler.java
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AbstractMoveRowHandler.java
@@ -10,8 +10,9 @@
  *******************************************************************************/
 package org.eclipse.vex.ui.internal.handlers;
 
+import org.eclipse.core.commands.ExecutionEvent;
 import org.eclipse.core.commands.ExecutionException;
-import org.eclipse.vex.core.internal.widget.swt.VexWidget;
+import org.eclipse.vex.core.internal.widget.IDocumentEditor;
 import org.eclipse.vex.core.provisional.dom.ContentPosition;
 import org.eclipse.vex.core.provisional.dom.ContentPositionRange;
 import org.eclipse.vex.ui.internal.handlers.VexHandlerUtil.SelectedRows;
@@ -25,23 +26,23 @@
 public abstract class AbstractMoveRowHandler extends AbstractVexWidgetHandler {
 
 	@Override
-	public void execute(final VexWidget widget) throws ExecutionException {
-		final VexHandlerUtil.SelectedRows selected = VexHandlerUtil.getSelectedTableRows(widget);
+	public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+		final VexHandlerUtil.SelectedRows selected = VexHandlerUtil.getSelectedTableRows(editor);
 
 		if (selected.getRows() == null || targetRow(selected) == null) {
 			return;
 		}
 
-		widget.doWork(new Runnable() {
+		editor.doWork(new Runnable() {
 			@Override
 			public void run() {
 				final ContentPositionRange range = VexHandlerUtil.getOuterRange(targetRow(selected));
-				widget.moveTo(range.getStartPosition());
-				widget.moveTo(range.getEndPosition(), true);
-				widget.cutSelection();
+				editor.moveTo(range.getStartPosition());
+				editor.moveTo(range.getEndPosition(), true);
+				editor.cutSelection();
 
-				widget.moveTo(target(selected));
-				widget.paste();
+				editor.moveTo(target(selected));
+				editor.paste();
 			}
 
 		}, true);
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AbstractNavigateTableCellHandler.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AbstractNavigateTableCellHandler.java
index b575d72..0cd2566 100644
--- a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AbstractNavigateTableCellHandler.java
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AbstractNavigateTableCellHandler.java
@@ -12,11 +12,12 @@
 
 import java.util.NoSuchElementException;
 
+import org.eclipse.core.commands.ExecutionEvent;
 import org.eclipse.core.commands.ExecutionException;
 import org.eclipse.vex.core.IFilter;
 import org.eclipse.vex.core.internal.css.CSS;
 import org.eclipse.vex.core.internal.css.StyleSheet;
-import org.eclipse.vex.core.internal.widget.swt.VexWidget;
+import org.eclipse.vex.core.internal.widget.IDocumentEditor;
 import org.eclipse.vex.core.provisional.dom.ContentPosition;
 import org.eclipse.vex.core.provisional.dom.IAxis;
 import org.eclipse.vex.core.provisional.dom.IElement;
@@ -31,8 +32,8 @@
 public abstract class AbstractNavigateTableCellHandler extends AbstractVexWidgetHandler {
 
 	@Override
-	public void execute(final VexWidget widget) throws ExecutionException {
-		final IAxis<? extends IParent> parentTableRows = widget.getCurrentElement().ancestors().matching(displayedAsTableRow(widget.getStyleSheet()));
+	public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+		final IAxis<? extends IParent> parentTableRows = editor.getCurrentElement().ancestors().matching(displayedAsTableRow(editor.getTableModel().getStyleSheet()));
 		final IElement tableRow;
 		try {
 			tableRow = (IElement) parentTableRows.first();
@@ -40,21 +41,21 @@
 			return;
 		}
 
-		final ContentPosition position = widget.getCaretPosition();
-		navigate(widget, tableRow, position);
+		final ContentPosition position = editor.getCaretPosition();
+		navigate(editor, tableRow, position);
 	}
 
 	/**
 	 * Navigates either to the next or previous table cell.
 	 *
-	 * @param widget
+	 * @param editor
 	 *            the Vex widget containing the document
 	 * @param tableRow
 	 *            the current row
 	 * @param offset
 	 *            the current offset
 	 */
-	protected abstract void navigate(VexWidget widget, IElement tableRow, ContentPosition position);
+	protected abstract void navigate(IDocumentEditor editor, IElement tableRow, ContentPosition position);
 
 	private static IFilter<INode> displayedAsTableRow(final StyleSheet stylesheet) {
 		return new IFilter<INode>() {
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AbstractRemoveTableCellsHandler.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AbstractRemoveTableCellsHandler.java
index 74f658a..f80166c 100644
--- a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AbstractRemoveTableCellsHandler.java
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AbstractRemoveTableCellsHandler.java
@@ -12,13 +12,14 @@
 
 import java.util.List;
 
+import org.eclipse.core.commands.ExecutionEvent;
 import org.eclipse.core.commands.ExecutionException;
-import org.eclipse.vex.core.internal.widget.swt.VexWidget;
+import org.eclipse.vex.core.internal.widget.IDocumentEditor;
 import org.eclipse.vex.core.provisional.dom.ContentPositionRange;
 
 /**
  * Deletes a given list of table cells (see
- * {@link #collectCellsToDelete(VexWidget, org.eclipse.vex.ui.internal.handlers.VexHandlerUtil.RowColumnInfo)} ).
+ * {@link #collectCellsToDelete(IDocumentEditor, org.eclipse.vex.ui.internal.handlers.VexHandlerUtil.RowColumnInfo)} ).
  *
  * @see RemoveColumnHandler
  * @see RemoveRowHandler
@@ -26,41 +27,41 @@
 public abstract class AbstractRemoveTableCellsHandler extends AbstractVexWidgetHandler {
 
 	@Override
-	public void execute(final VexWidget widget) throws ExecutionException {
-		widget.doWork(new Runnable() {
+	public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+		editor.doWork(new Runnable() {
 			@Override
 			public void run() {
 
-				final VexHandlerUtil.RowColumnInfo rcInfo = VexHandlerUtil.getRowColumnInfo(widget);
+				final VexHandlerUtil.RowColumnInfo rcInfo = VexHandlerUtil.getRowColumnInfo(editor);
 
 				if (rcInfo == null) {
 					return;
 				}
 
-				deleteCells(widget, collectCellsToDelete(widget, rcInfo));
+				deleteCells(editor, collectCellsToDelete(editor, rcInfo));
 			}
 
 		});
 	}
 
 	/**
-	 * @param widget
+	 * @param editor
 	 *            the Vex widget
 	 * @param rcInfo
 	 *            row/column info of the current table cell
 	 * @return list of elements to delete
 	 */
-	protected abstract List<Object> collectCellsToDelete(VexWidget widget, VexHandlerUtil.RowColumnInfo rcInfo);
+	protected abstract List<Object> collectCellsToDelete(IDocumentEditor editor, VexHandlerUtil.RowColumnInfo rcInfo);
 
-	private void deleteCells(final VexWidget widget, final List<Object> cellsToDelete) {
+	private void deleteCells(final IDocumentEditor editor, final List<Object> cellsToDelete) {
 		// Iterate the deletions in reverse, so that we don't mess up offsets
 		// that are in anonymous cells, which are not stored as Positions.
 		for (int i = cellsToDelete.size() - 1; i >= 0; i--) {
 			final Object cell = cellsToDelete.get(i);
 			final ContentPositionRange range = VexHandlerUtil.getOuterRange(cell);
-			widget.moveTo(range.getStartPosition());
-			widget.moveTo(range.getEndPosition(), true);
-			widget.deleteSelection();
+			editor.moveTo(range.getStartPosition());
+			editor.moveTo(range.getEndPosition(), true);
+			editor.deleteSelection();
 		}
 	}
 }
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AbstractVexWidgetHandler.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AbstractVexWidgetHandler.java
index 21601ae..0783bc3 100644
--- a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AbstractVexWidgetHandler.java
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AbstractVexWidgetHandler.java
@@ -19,6 +19,7 @@
 import org.eclipse.ui.IWorkbenchWindow;
 import org.eclipse.ui.menus.UIElement;
 import org.eclipse.ui.services.IServiceScopes;
+import org.eclipse.vex.core.internal.widget.IDocumentEditor;
 import org.eclipse.vex.core.internal.widget.swt.IVexWidgetHandler;
 import org.eclipse.vex.core.internal.widget.swt.VexWidget;
 import org.eclipse.vex.core.provisional.dom.IElement;
@@ -33,12 +34,12 @@
 
 	@Override
 	public Object execute(final ExecutionEvent event) throws ExecutionException {
-		execute(VexHandlerUtil.computeWidget(event));
+		execute(event, VexHandlerUtil.getDocumentEditor(event));
 		return null;
 	}
 
 	@Override
-	public abstract void execute(VexWidget widget) throws ExecutionException;
+	public abstract void execute(ExecutionEvent event, IDocumentEditor editor) throws ExecutionException;
 
 	/**
 	 * Helper method to implement {@link org.eclipse.ui.commands.IElementUpdater}: Updates the name of the UI element
@@ -58,12 +59,12 @@
 		}
 
 		final IWorkbenchWindow window = (IWorkbenchWindow) windowObject;
-		final VexWidget widget = VexHandlerUtil.computeWidget(window);
-		if (widget == null) {
+		final IDocumentEditor editor = VexHandlerUtil.getDocumentEditor(window);
+		if (editor == null) {
 			return;
 		}
 
-		final IElement currentElement = widget.getCurrentElement();
+		final IElement currentElement = editor.getCurrentElement();
 		final String name;
 		if (currentElement != null) {
 			name = currentElement.getPrefixedName();
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AddCommentHandler.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AddCommentHandler.java
index e423744..bc5bd16 100644
--- a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AddCommentHandler.java
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AddCommentHandler.java
@@ -1,28 +1,29 @@
-/*******************************************************************************

- * Copyright (c) 2012 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.ui.internal.handlers;

-

-import org.eclipse.core.commands.ExecutionException;

-import org.eclipse.vex.core.internal.widget.swt.VexWidget;

-

-/**

- * @author Florian Thienel

- */

-public class AddCommentHandler extends AbstractVexWidgetHandler {

-

-	@Override

-	public void execute(final VexWidget widget) throws ExecutionException {

-		if (widget.canInsertComment()) {

-			widget.insertComment();

-		}

-	}

-

-}

+/*******************************************************************************
+ * Copyright (c) 2012 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.ui.internal.handlers;
+
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.vex.core.internal.widget.IDocumentEditor;
+
+/**
+ * @author Florian Thienel
+ */
+public class AddCommentHandler extends AbstractVexWidgetHandler {
+
+	@Override
+	public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+		if (editor.canInsertComment()) {
+			editor.insertComment();
+		}
+	}
+
+}
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AddElementHandler.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AddElementHandler.java
index 41ca65a..efd293d 100644
--- a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AddElementHandler.java
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AddElementHandler.java
@@ -10,8 +10,13 @@
  *******************************************************************************/
 package org.eclipse.vex.ui.internal.handlers;
 
+import org.eclipse.core.commands.ExecutionEvent;
 import org.eclipse.core.commands.ExecutionException;
-import org.eclipse.vex.core.internal.widget.swt.VexWidget;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.handlers.HandlerUtil;
+import org.eclipse.vex.core.internal.core.Rectangle;
+import org.eclipse.vex.core.internal.widget.IDocumentEditor;
 import org.eclipse.vex.ui.internal.swt.ContentAssist;
 
 /**
@@ -20,8 +25,11 @@
 public class AddElementHandler extends AbstractVexWidgetHandler {
 
 	@Override
-	public void execute(final VexWidget widget) throws ExecutionException {
-		ContentAssist.openAddElementsContentAssist(widget);
+	public void execute(final ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+		final Shell shell = HandlerUtil.getActiveShell(event);
+		final Rectangle caretArea = VexHandlerUtil.getCaretArea(event);
+		final Point location = new Point(caretArea.getX(), caretArea.getY());
+		ContentAssist.openAddElementsContentAssist(shell, editor, location);
 	}
 
 }
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AddProcessingInstructionHandler.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AddProcessingInstructionHandler.java
index f73e3f8..25ccd1d 100644
--- a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AddProcessingInstructionHandler.java
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/AddProcessingInstructionHandler.java
@@ -10,10 +10,11 @@
  *******************************************************************************/
 package org.eclipse.vex.ui.internal.handlers;
 
+import org.eclipse.core.commands.ExecutionEvent;
 import org.eclipse.core.commands.ExecutionException;
 import org.eclipse.jface.window.Window;
 import org.eclipse.swt.widgets.Shell;
-import org.eclipse.vex.core.internal.widget.swt.VexWidget;
+import org.eclipse.vex.core.internal.widget.IDocumentEditor;
 import org.eclipse.vex.core.provisional.dom.DocumentValidationException;
 import org.eclipse.vex.ui.internal.VexPlugin;
 import org.eclipse.vex.ui.internal.swt.ProcessingInstrDialog;
@@ -21,15 +22,15 @@
 public class AddProcessingInstructionHandler extends AbstractVexWidgetHandler {
 
 	@Override
-	public void execute(final VexWidget widget) throws ExecutionException {
-		if (widget.canInsertProcessingInstruction()) {
+	public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+		if (editor.canInsertProcessingInstruction()) {
 			final Shell shell = VexPlugin.getDefault().getWorkbench().getActiveWorkbenchWindow().getShell();
 			final ProcessingInstrDialog dialog = new ProcessingInstrDialog(shell, "");
 			dialog.create();
 
-			if (dialog.open() == Window.OK && widget.canInsertProcessingInstruction()) {
+			if (dialog.open() == Window.OK && editor.canInsertProcessingInstruction()) {
 				try {
-					widget.insertProcessingInstruction(dialog.getTarget());
+					editor.insertProcessingInstruction(dialog.getTarget());
 				} catch (final DocumentValidationException e) {
 
 				}
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/ConvertElementHandler.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/ConvertElementHandler.java
index caffa5a..74a4401 100644
--- a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/ConvertElementHandler.java
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/ConvertElementHandler.java
@@ -12,10 +12,15 @@
 
 import java.util.Map;
 
+import org.eclipse.core.commands.ExecutionEvent;
 import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.widgets.Shell;
 import org.eclipse.ui.commands.IElementUpdater;
+import org.eclipse.ui.handlers.HandlerUtil;
 import org.eclipse.ui.menus.UIElement;
-import org.eclipse.vex.core.internal.widget.swt.VexWidget;
+import org.eclipse.vex.core.internal.core.Rectangle;
+import org.eclipse.vex.core.internal.widget.IDocumentEditor;
 import org.eclipse.vex.ui.internal.swt.ContentAssist;
 
 /**
@@ -29,8 +34,11 @@
 	private static final String LABEL_ID = "command.convertElement.dynamicName"; //$NON-NLS-1$
 
 	@Override
-	public void execute(final VexWidget widget) throws ExecutionException {
-		ContentAssist.openQuickFixContentAssist(widget);
+	public void execute(final ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+		final Shell shell = HandlerUtil.getActiveShell(event);
+		final Rectangle caretArea = VexHandlerUtil.getCaretArea(event);
+		final Point location = new Point(caretArea.getX(), caretArea.getY());
+		ContentAssist.openQuickFixContentAssist(shell, editor, location);
 	}
 
 	@Override
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/DuplicateSelectionHandler.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/DuplicateSelectionHandler.java
index 3e08714..9ab110e 100644
--- a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/DuplicateSelectionHandler.java
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/DuplicateSelectionHandler.java
@@ -10,8 +10,9 @@
  *******************************************************************************/
 package org.eclipse.vex.ui.internal.handlers;
 
+import org.eclipse.core.commands.ExecutionEvent;
 import org.eclipse.core.commands.ExecutionException;
-import org.eclipse.vex.core.internal.widget.swt.VexWidget;
+import org.eclipse.vex.core.internal.widget.IDocumentEditor;
 import org.eclipse.vex.core.provisional.dom.ContentPosition;
 import org.eclipse.vex.core.provisional.dom.INode;
 
@@ -21,29 +22,29 @@
 public class DuplicateSelectionHandler extends AbstractVexWidgetHandler {
 
 	@Override
-	public void execute(final VexWidget widget) throws ExecutionException {
-		widget.doWork(new Runnable() {
+	public void execute(final ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+		editor.doWork(new Runnable() {
 			@Override
 			public void run() {
-				if (!widget.hasSelection()) {
-					final INode node = widget.getCurrentNode();
+				if (!editor.hasSelection()) {
+					final INode node = editor.getCurrentNode();
 
 					// Can't duplicate the document
 					if (node.getParent() == null) {
 						return;
 					}
 
-					widget.moveTo(node.getStartPosition());
-					widget.moveTo(node.getEndPosition().moveBy(+1), true);
+					editor.moveTo(node.getStartPosition());
+					editor.moveTo(node.getEndPosition().moveBy(+1), true);
 				}
 
-				widget.copySelection();
-				final ContentPosition startPosition = widget.getSelectedPositionRange().getEndPosition().moveBy(1);
-				widget.moveTo(startPosition);
-				widget.paste();
-				final ContentPosition endPosition = widget.getCaretPosition();
-				widget.moveTo(startPosition);
-				widget.moveTo(endPosition, true);
+				editor.copySelection();
+				final ContentPosition startPosition = editor.getSelectedPositionRange().getEndPosition();
+				editor.moveTo(startPosition);
+				editor.paste();
+				final ContentPosition endPosition = editor.getCaretPosition();
+				editor.moveTo(startPosition);
+				editor.moveTo(endPosition, true);
 			}
 		});
 	}
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/JoinHandler.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/JoinHandler.java
index cf2c25f..60e2d01 100644
--- a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/JoinHandler.java
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/JoinHandler.java
@@ -10,8 +10,9 @@
  *******************************************************************************/
 package org.eclipse.vex.ui.internal.handlers;
 
+import org.eclipse.core.commands.ExecutionEvent;
 import org.eclipse.core.commands.ExecutionException;
-import org.eclipse.vex.core.internal.widget.swt.VexWidget;
+import org.eclipse.vex.core.internal.widget.IDocumentEditor;
 
 /**
  * @author Florian Thienel
@@ -19,9 +20,9 @@
 public class JoinHandler extends AbstractVexWidgetHandler {
 
 	@Override
-	public void execute(final VexWidget widget) throws ExecutionException {
-		if (widget.canJoin()) {
-			widget.join();
+	public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+		if (editor.canJoin()) {
+			editor.join();
 		}
 	}
 
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/MoveSelectionUpHandler.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/MoveSelectionUpHandler.java
index f3df30e..5bb1741 100644
--- a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/MoveSelectionUpHandler.java
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/MoveSelectionUpHandler.java
@@ -10,10 +10,11 @@
  *******************************************************************************/
 package org.eclipse.vex.ui.internal.handlers;
 
+import org.eclipse.core.commands.ExecutionEvent;
 import org.eclipse.core.commands.ExecutionException;
 import org.eclipse.vex.core.IFilter;
 import org.eclipse.vex.core.internal.css.IWhitespacePolicy;
-import org.eclipse.vex.core.internal.widget.swt.VexWidget;
+import org.eclipse.vex.core.internal.widget.IDocumentEditor;
 import org.eclipse.vex.core.provisional.dom.ContentRange;
 import org.eclipse.vex.core.provisional.dom.IAxis;
 import org.eclipse.vex.core.provisional.dom.INode;
@@ -27,17 +28,17 @@
 public class MoveSelectionUpHandler extends AbstractVexWidgetHandler {
 
 	@Override
-	public void execute(final VexWidget widget) throws ExecutionException {
-		final ContentRange selectedRange = widget.getSelectedRange();
-		final IAxis<? extends IParent> parentsContainingSelection = widget.getCurrentElement().ancestors().matching(containingRange(selectedRange));
+	public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+		final ContentRange selectedRange = editor.getSelectedRange();
+		final IAxis<? extends IParent> parentsContainingSelection = editor.getCurrentElement().ancestors().matching(containingRange(selectedRange));
 
 		// expand the selection until a parent has other block-children
-		final IWhitespacePolicy policy = widget.getWhitespacePolicy();
+		final IWhitespacePolicy policy = editor.getWhitespacePolicy();
 		for (final IParent parent : parentsContainingSelection) {
 			final IAxis<? extends INode> blockChildren = parent.children().matching(displayedAsBlock(policy));
 			if (blockChildren.isEmpty()) {
-				widget.moveTo(parent.getStartPosition(), false);
-				widget.moveTo(parent.getEndPosition(), true);
+				editor.moveTo(parent.getStartPosition(), false);
+				editor.moveTo(parent.getEndPosition(), true);
 			} else {
 				break;
 			}
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/NextTableCellHandler.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/NextTableCellHandler.java
index 618c5ee..fe98183 100644
--- a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/NextTableCellHandler.java
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/NextTableCellHandler.java
@@ -13,7 +13,7 @@
 
 import java.util.NoSuchElementException;
 
-import org.eclipse.vex.core.internal.widget.swt.VexWidget;
+import org.eclipse.vex.core.internal.widget.IDocumentEditor;
 import org.eclipse.vex.core.provisional.dom.ContentPosition;
 import org.eclipse.vex.core.provisional.dom.IElement;
 
@@ -25,11 +25,11 @@
 public class NextTableCellHandler extends AbstractNavigateTableCellHandler {
 
 	@Override
-	protected void navigate(final VexWidget widget, final IElement tableRow, final ContentPosition position) {
+	protected void navigate(final IDocumentEditor editor, final IElement tableRow, final ContentPosition position) {
 		// in this row
 		for (final IElement cell : tableRow.childElements()) {
 			if (cell.getStartPosition().isAfter(position)) {
-				widget.moveTo(cell.getStartPosition().moveBy(1));
+				editor.moveTo(cell.getStartPosition().moveBy(1));
 				return;
 			}
 		}
@@ -40,7 +40,7 @@
 				final IElement firstCell = firstCellOf(siblingRow);
 
 				if (firstCell != null) {
-					widget.moveTo(firstCell.getStartPosition().moveBy(1));
+					editor.moveTo(firstCell.getStartPosition().moveBy(1));
 				} else {
 					System.out.println("TODO - dup row into new empty row");
 				}
@@ -49,7 +49,7 @@
 		}
 
 		// We didn't find a "next row", so let's dup the current one
-		VexHandlerUtil.duplicateTableRow(widget, tableRow, false);
+		VexHandlerUtil.duplicateTableRow(editor, tableRow, false);
 	}
 
 	private static IElement firstCellOf(final IElement tableRow) {
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/PreviousTableCellHandler.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/PreviousTableCellHandler.java
index f42e19e..e2025a8 100644
--- a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/PreviousTableCellHandler.java
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/PreviousTableCellHandler.java
@@ -12,7 +12,7 @@
 
 import java.util.NoSuchElementException;
 
-import org.eclipse.vex.core.internal.widget.swt.VexWidget;
+import org.eclipse.vex.core.internal.widget.IDocumentEditor;
 import org.eclipse.vex.core.provisional.dom.ContentPosition;
 import org.eclipse.vex.core.provisional.dom.IElement;
 
@@ -24,7 +24,7 @@
 public class PreviousTableCellHandler extends AbstractNavigateTableCellHandler {
 
 	@Override
-	protected void navigate(final VexWidget widget, final IElement tableRow, final ContentPosition position) {
+	protected void navigate(final IDocumentEditor editor, final IElement tableRow, final ContentPosition position) {
 		IElement siblingCell = null;
 		for (final IElement cell : tableRow.childElements()) {
 			if (cell.getEndPosition().isAfterOrEquals(position)) {
@@ -35,7 +35,7 @@
 
 		// in this row
 		if (siblingCell != null) {
-			widget.moveTo(siblingCell.getStartPosition().moveBy(1));
+			editor.moveTo(siblingCell.getStartPosition().moveBy(1));
 			return;
 		}
 
@@ -51,7 +51,7 @@
 		if (siblingRow != null) {
 			final IElement lastCell = lastCellOf(siblingRow);
 			if (lastCell != null) {
-				widget.moveTo(lastCell.getStartPosition().moveBy(1));
+				editor.moveTo(lastCell.getStartPosition().moveBy(1));
 			}
 		}
 	}
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/RemoveColumnHandler.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/RemoveColumnHandler.java
index e4e280d..755c0f0 100644
--- a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/RemoveColumnHandler.java
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/RemoveColumnHandler.java
@@ -14,7 +14,7 @@
 import java.util.List;
 
 import org.eclipse.vex.core.internal.layout.LayoutUtils.ElementOrRange;
-import org.eclipse.vex.core.internal.widget.swt.VexWidget;
+import org.eclipse.vex.core.internal.widget.IDocumentEditor;
 
 /**
  * Deletes current column.
@@ -22,9 +22,9 @@
 public class RemoveColumnHandler extends AbstractRemoveTableCellsHandler {
 
 	@Override
-	protected List<Object> collectCellsToDelete(final VexWidget widget, final VexHandlerUtil.RowColumnInfo rcInfo) {
+	protected List<Object> collectCellsToDelete(final IDocumentEditor editor, final VexHandlerUtil.RowColumnInfo rcInfo) {
 		final List<Object> cellsToDelete = new ArrayList<Object>();
-		VexHandlerUtil.iterateTableCells(widget, new TableCellCallbackAdapter() {
+		VexHandlerUtil.iterateTableCells(editor, new TableCellCallbackAdapter() {
 			@Override
 			public void onCell(final ElementOrRange row, final ElementOrRange cell, final int rowIndex, final int cellIndex) {
 				if (cellIndex == rcInfo.cellIndex) {
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/RemoveRowHandler.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/RemoveRowHandler.java
index 8d10f7c..0e28308 100644
--- a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/RemoveRowHandler.java
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/RemoveRowHandler.java
@@ -12,7 +12,7 @@
 
 import java.util.List;
 
-import org.eclipse.vex.core.internal.widget.swt.VexWidget;
+import org.eclipse.vex.core.internal.widget.IDocumentEditor;
 
 /**
  * Deletes selected row(s).
@@ -20,8 +20,8 @@
 public class RemoveRowHandler extends AbstractRemoveTableCellsHandler {
 
 	@Override
-	protected List<Object> collectCellsToDelete(final VexWidget widget, final VexHandlerUtil.RowColumnInfo rcInfo) {
-		return VexHandlerUtil.getSelectedTableRows(widget).getRows();
+	protected List<Object> collectCellsToDelete(final IDocumentEditor editor, final VexHandlerUtil.RowColumnInfo rcInfo) {
+		return VexHandlerUtil.getSelectedTableRows(editor).getRows();
 	}
 
 }
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/RemoveTagHandler.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/RemoveTagHandler.java
index 57cade6..50770e7 100644
--- a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/RemoveTagHandler.java
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/RemoveTagHandler.java
@@ -13,10 +13,11 @@
 
 import java.util.Map;
 
+import org.eclipse.core.commands.ExecutionEvent;
 import org.eclipse.core.commands.ExecutionException;
 import org.eclipse.ui.commands.IElementUpdater;
 import org.eclipse.ui.menus.UIElement;
-import org.eclipse.vex.core.internal.widget.swt.VexWidget;
+import org.eclipse.vex.core.internal.widget.IDocumentEditor;
 
 /**
  * Removes the current tag: deletes the element but adds its content to the parent element.
@@ -30,9 +31,9 @@
 	private static final String PARTSITE_SCOPE_DYNAMIC_LABEL_ID = "command.removeTag.dynamicName"; //$NON-NLS-1$
 
 	@Override
-	public void execute(final VexWidget widget) throws ExecutionException {
-		if (widget.canUnwrap()) {
-			widget.unwrap();
+	public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+		if (editor.canUnwrap()) {
+			editor.unwrap();
 		}
 	}
 
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/SetProcessingInstructionTargetHandler.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/SetProcessingInstructionTargetHandler.java
index 1d4c304..310d060 100644
--- a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/SetProcessingInstructionTargetHandler.java
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/SetProcessingInstructionTargetHandler.java
@@ -10,10 +10,11 @@
  *******************************************************************************/
 package org.eclipse.vex.ui.internal.handlers;
 
+import org.eclipse.core.commands.ExecutionEvent;
 import org.eclipse.core.commands.ExecutionException;
 import org.eclipse.jface.window.Window;
 import org.eclipse.swt.widgets.Shell;
-import org.eclipse.vex.core.internal.widget.swt.VexWidget;
+import org.eclipse.vex.core.internal.widget.IDocumentEditor;
 import org.eclipse.vex.core.provisional.dom.DocumentValidationException;
 import org.eclipse.vex.core.provisional.dom.IProcessingInstruction;
 import org.eclipse.vex.ui.internal.VexPlugin;
@@ -22,16 +23,16 @@
 public class SetProcessingInstructionTargetHandler extends AbstractVexWidgetHandler {
 
 	@Override
-	public void execute(final VexWidget widget) throws ExecutionException {
-		if (widget.getCurrentNode() instanceof IProcessingInstruction) {
-			final IProcessingInstruction pi = (IProcessingInstruction) widget.getCurrentNode();
+	public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+		if (editor.getCurrentNode() instanceof IProcessingInstruction) {
+			final IProcessingInstruction pi = (IProcessingInstruction) editor.getCurrentNode();
 			final Shell shell = VexPlugin.getDefault().getWorkbench().getActiveWorkbenchWindow().getShell();
 			final ProcessingInstrDialog dialog = new ProcessingInstrDialog(shell, pi.getTarget());
 			dialog.create();
 
 			if (dialog.open() == Window.OK) {
 				try {
-					widget.editProcessingInstruction(dialog.getTarget(), null);
+					editor.editProcessingInstruction(dialog.getTarget(), null);
 				} catch (final DocumentValidationException e) {
 
 				}
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/SplitBlockElementHandler.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/SplitBlockElementHandler.java
index 7371509..e8727f4 100644
--- a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/SplitBlockElementHandler.java
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/SplitBlockElementHandler.java
@@ -11,9 +11,13 @@
  *******************************************************************************/
 package org.eclipse.vex.ui.internal.handlers;
 
+import org.eclipse.core.commands.ExecutionEvent;
 import org.eclipse.core.commands.ExecutionException;
-import org.eclipse.vex.core.internal.widget.IVexWidget;
-import org.eclipse.vex.core.internal.widget.swt.VexWidget;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.handlers.HandlerUtil;
+import org.eclipse.vex.core.internal.core.Rectangle;
+import org.eclipse.vex.core.internal.widget.IDocumentEditor;
 import org.eclipse.vex.core.provisional.dom.ContentPosition;
 import org.eclipse.vex.core.provisional.dom.INode;
 import org.eclipse.vex.ui.internal.swt.ContentAssist;
@@ -27,19 +31,22 @@
 public class SplitBlockElementHandler extends AbstractVexWidgetHandler {
 
 	@Override
-	public void execute(final VexWidget widget) throws ExecutionException {
-		if (widget.isReadOnly()) {
+	public void execute(final ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+		if (editor.isReadOnly()) {
 			return;
 		}
 
-		final INode currentNode = widget.getCurrentNode();
-		if (widget.canSplit()) {
-			splitElement(widget, currentNode);
+		final INode currentNode = editor.getCurrentNode();
+		if (editor.canSplit()) {
+			splitElement(editor, currentNode);
 		} else {
 			final ContentPosition targetPosition = currentNode.getEndPosition().moveBy(1);
-			if (widget.getDocument().getRootElement().containsPosition(targetPosition)) {
-				widget.moveTo(targetPosition);
-				ContentAssist.openAddElementsContentAssist(widget);
+			if (editor.getDocument().getRootElement().containsPosition(targetPosition)) {
+				editor.moveTo(targetPosition);
+				final Shell shell = HandlerUtil.getActiveShell(event);
+				final Rectangle caretArea = VexHandlerUtil.getCaretArea(event);
+				final Point location = new Point(caretArea.getX(), caretArea.getY());
+				ContentAssist.openAddElementsContentAssist(shell, editor, location);
 			}
 		}
 	}
@@ -47,20 +54,20 @@
 	/**
 	 * Splits the given element.
 	 *
-	 * @param vexWidget
+	 * @param editor
 	 *            IVexWidget containing the document.
 	 * @param node
 	 *            Node to be split.
 	 */
-	protected void splitElement(final IVexWidget vexWidget, final INode node) {
-		vexWidget.doWork(new Runnable() {
+	protected void splitElement(final IDocumentEditor editor, final INode node) {
+		editor.doWork(new Runnable() {
 			@Override
 			public void run() {
-				final boolean isPreformatted = vexWidget.getWhitespacePolicy().isPre(node);
+				final boolean isPreformatted = editor.getWhitespacePolicy().isPre(node);
 				if (isPreformatted) {
-					vexWidget.insertText("\n");
+					editor.insertText("\n");
 				} else {
-					vexWidget.split();
+					editor.split();
 				}
 			}
 		});
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/SplitItemHandler.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/SplitItemHandler.java
index 8f05ca3..469355c 100644
--- a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/SplitItemHandler.java
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/SplitItemHandler.java
@@ -12,11 +12,12 @@
 
 import java.util.NoSuchElementException;
 
+import org.eclipse.core.commands.ExecutionEvent;
 import org.eclipse.core.commands.ExecutionException;
 import org.eclipse.vex.core.IFilter;
 import org.eclipse.vex.core.internal.css.CSS;
 import org.eclipse.vex.core.internal.css.StyleSheet;
-import org.eclipse.vex.core.internal.widget.swt.VexWidget;
+import org.eclipse.vex.core.internal.widget.IDocumentEditor;
 import org.eclipse.vex.core.provisional.dom.IAxis;
 import org.eclipse.vex.core.provisional.dom.INode;
 import org.eclipse.vex.core.provisional.dom.IParent;
@@ -30,9 +31,9 @@
 public class SplitItemHandler extends SplitBlockElementHandler {
 
 	@Override
-	public void execute(final VexWidget widget) throws ExecutionException {
-		final StyleSheet stylesheet = widget.getStyleSheet();
-		final IAxis<? extends IParent> parentTableRowOrListItems = widget.getCurrentElement().ancestors().matching(displayedAsTableRowOrListItem(stylesheet));
+	public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+		final StyleSheet stylesheet = editor.getTableModel().getStyleSheet();
+		final IAxis<? extends IParent> parentTableRowOrListItems = editor.getCurrentElement().ancestors().matching(displayedAsTableRowOrListItem(stylesheet));
 
 		final IParent firstTableRowOrListItem;
 		try {
@@ -43,9 +44,9 @@
 
 		final String displayStyle = stylesheet.getStyles(firstTableRowOrListItem).getDisplay();
 		if (displayStyle.equals(CSS.TABLE_ROW)) {
-			new AddRowBelowHandler().execute(widget);
+			new AddRowBelowHandler().execute(event, editor);
 		} else if (displayStyle.equals(CSS.LIST_ITEM)) {
-			splitElement(widget, firstTableRowOrListItem);
+			splitElement(editor, firstTableRowOrListItem);
 		}
 	}
 
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/StyleMenu.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/StyleMenu.java
index 098c2f6..2e4afbc 100644
--- a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/StyleMenu.java
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/StyleMenu.java
@@ -33,7 +33,7 @@
 	public void fill(final Menu menu, final int index) {
 		final IWorkbench workbench = PlatformUI.getWorkbench();
 		final IWorkbenchWindow window = workbench.getActiveWorkbenchWindow();
-		final VexEditor editor = VexHandlerUtil.computeVexEditor(window);
+		final VexEditor editor = VexHandlerUtil.getActiveVexEditor(window);
 		if (editor == null) {
 			return;
 		}
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/TextEditingHandlerFactory.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/TextEditingHandlerFactory.java
index dace428..4a9da57 100644
--- a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/TextEditingHandlerFactory.java
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/TextEditingHandlerFactory.java
@@ -1,544 +1,545 @@
-/*******************************************************************************

- * Copyright (c) 2013 Holger Voormann 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:

- *     Holger Voormann - initial API and implementation

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

-package org.eclipse.vex.ui.internal.handlers;

-

-import org.eclipse.core.commands.ExecutionException;

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

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

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

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

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

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

-import org.eclipse.vex.core.internal.widget.swt.VexWidget;

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

-import org.eclipse.vex.ui.internal.VexPlugin;

-

-/**

- * Factory for following command handlers:

- * <ul>

- * <li><em>Go to</em> and <em>Select</em>:

- * <ul>

- * <li>Previous/Next Column (<em>column</em> in terms of <em>character</em>)</li>

- * <li>Line Up/Down</li>

- * <li>Previous/Next Word</li>

- * <li>Line Start/End</li>

- * <li>Page Up/Down</li>

- * <li>Text Start/End (<em>text</em> in terms of <em>document</em>)</li>

- * </ul>

- * </li>

- * <li><em>Delete</em>:

- * <ul>

- * <li>Previous/Next (Character)</li>

- * <li>Previous/Next Word</li>

- * <li>Line</li>

- * <li>To Beginning/End of Line</li>

- * </ul>

- * </li>

- * <li><em>Cut</em>:

- * <ul>

- * <li>Line</li>

- * <li>Delete to Beginning/End of Line</li>

- * </ul>

- * </li>

- * </ul>

- */

-public class TextEditingHandlerFactory implements IExecutableExtensionFactory, IExecutableExtension {

-

-	private String id;

-

-	@Override

-	public Object create() throws CoreException {

-

-		// go to handlers

-		if (PreviousColumn.class.getSimpleName().equals(id)) {

-			return new PreviousColumn();

-		}

-		if (NextColumn.class.getSimpleName().equals(id)) {

-			return new NextColumn();

-		}

-		if (LineUp.class.getSimpleName().equals(id)) {

-			return new LineUp();

-		}

-		if (LineDown.class.getSimpleName().equals(id)) {

-			return new LineDown();

-		}

-		if (PreviousWord.class.getSimpleName().equals(id)) {

-			return new PreviousWord();

-		}

-		if (NextWord.class.getSimpleName().equals(id)) {

-			return new NextWord();

-		}

-		if (LineStart.class.getSimpleName().equals(id)) {

-			return new LineStart();

-		}

-		if (LineEnd.class.getSimpleName().equals(id)) {

-			return new LineEnd();

-		}

-		if (PageUp.class.getSimpleName().equals(id)) {

-			return new PageUp();

-		}

-		if (PageDown.class.getSimpleName().equals(id)) {

-			return new PageDown();

-		}

-		if (TextStart.class.getSimpleName().equals(id)) {

-			return new TextStart();

-		}

-		if (TextEnd.class.getSimpleName().equals(id)) {

-			return new TextEnd();

-		}

-

-		// select handlers

-		if (SelectPreviousColumn.class.getSimpleName().equals(id)) {

-			return new SelectPreviousColumn();

-		}

-		if (SelectNextColumn.class.getSimpleName().equals(id)) {

-			return new SelectNextColumn();

-		}

-		if (SelectLineUp.class.getSimpleName().equals(id)) {

-			return new SelectLineUp();

-		}

-		if (SelectLineDown.class.getSimpleName().equals(id)) {

-			return new SelectLineDown();

-		}

-		if (SelectPreviousWord.class.getSimpleName().equals(id)) {

-			return new SelectPreviousWord();

-		}

-		if (SelectNextWord.class.getSimpleName().equals(id)) {

-			return new SelectNextWord();

-		}

-		if (SelectLineStart.class.getSimpleName().equals(id)) {

-			return new SelectLineStart();

-		}

-		if (SelectLineEnd.class.getSimpleName().equals(id)) {

-			return new SelectLineEnd();

-		}

-		if (SelectPageUp.class.getSimpleName().equals(id)) {

-			return new SelectPageUp();

-		}

-		if (SelectPageDown.class.getSimpleName().equals(id)) {

-			return new SelectPageDown();

-		}

-		if (SelectTextStart.class.getSimpleName().equals(id)) {

-			return new SelectTextStart();

-		}

-		if (SelectTextEnd.class.getSimpleName().equals(id)) {

-			return new SelectTextEnd();

-		}

-

-		// delete handlers

-		if (DeletePrevious.class.getSimpleName().equals(id)) {

-			return new DeletePrevious();

-		}

-		if (DeleteNext.class.getSimpleName().equals(id)) {

-			return new DeleteNext();

-		}

-		if (DeletePreviousWord.class.getSimpleName().equals(id)) {

-			return new DeletePreviousWord();

-		}

-		if (DeleteNextWord.class.getSimpleName().equals(id)) {

-			return new DeleteNextWord();

-		}

-		if (DeleteLine.class.getSimpleName().equals(id)) {

-			return new DeleteLine();

-		}

-		if (DeleteToBeginningOfLine.class.getSimpleName().equals(id)) {

-			return new DeleteToBeginningOfLine();

-		}

-		if (DeleteToEndOfLine.class.getSimpleName().equals(id)) {

-			return new DeleteToEndOfLine();

-		}

-

-		// cut handlers

-		if (CutLine.class.getSimpleName().equals(id)) {

-			return new CutLine();

-		}

-		if (CutLineToBeginning.class.getSimpleName().equals(id)) {

-			return new CutLineToBeginning();

-		}

-		if (CutLineToEnd.class.getSimpleName().equals(id)) {

-			return new CutLineToEnd();

-		}

-

-		// not available

-		throw new CoreException(new Status(IStatus.ERROR, VexPlugin.ID, 0, "Unknown id in data argument for " + getClass(), null)); //$NON-NLS-1$

-	}

-

-	@Override

-	public void setInitializationData(final IConfigurationElement config, final String propertyName, final Object data) throws CoreException {

-		if (data instanceof String) {

-			id = (String) data;

-			return;

-		}

-		throw new CoreException(new Status(IStatus.ERROR, VexPlugin.ID, 0, "Data argument must be a String for " + getClass(), null)); //$NON-NLS-1$

-	}

-

-	private static class PreviousColumn extends AbstractVexWidgetHandler {

-

-		@Override

-		public void execute(final VexWidget widget) throws ExecutionException {

-			widget.moveBy(-1);

-		}

-

-	}

-

-	private static class NextColumn extends AbstractVexWidgetHandler {

-

-		@Override

-		public void execute(final VexWidget widget) throws ExecutionException {

-			widget.moveBy(1);

-		}

-

-	}

-

-	private static class LineUp extends AbstractVexWidgetHandler {

-

-		@Override

-		public void execute(final VexWidget widget) throws ExecutionException {

-			widget.moveToPreviousLine(false);

-		}

-

-	}

-

-	private static class LineDown extends AbstractVexWidgetHandler {

-

-		@Override

-		public void execute(final VexWidget widget) throws ExecutionException {

-			widget.moveToNextLine(false);

-		}

-

-	}

-

-	private static class PreviousWord extends AbstractVexWidgetHandler {

-

-		@Override

-		public void execute(final VexWidget widget) throws ExecutionException {

-			widget.moveToPreviousWord(false);

-		}

-

-	}

-

-	private static class NextWord extends AbstractVexWidgetHandler {

-

-		@Override

-		public void execute(final VexWidget widget) throws ExecutionException {

-			widget.moveToNextWord(false);

-		}

-

-	}

-

-	private static class LineStart extends AbstractVexWidgetHandler {

-

-		@Override

-		public void execute(final VexWidget widget) throws ExecutionException {

-			widget.moveToLineStart(false);

-		}

-

-	}

-

-	private static class LineEnd extends AbstractVexWidgetHandler {

-

-		@Override

-		public void execute(final VexWidget widget) throws ExecutionException {

-			widget.moveToLineEnd(false);

-		}

-

-	}

-

-	private static class PageUp extends AbstractVexWidgetHandler {

-

-		@Override

-		public void execute(final VexWidget widget) throws ExecutionException {

-			widget.moveToPreviousPage(false);

-		}

-

-	}

-

-	private static class PageDown extends AbstractVexWidgetHandler {

-

-		@Override

-		public void execute(final VexWidget widget) throws ExecutionException {

-			widget.moveToNextPage(false);

-		}

-

-	}

-

-	private static class TextStart extends AbstractVexWidgetHandler {

-

-		@Override

-		public void execute(final VexWidget widget) throws ExecutionException {

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

-		}

-

-	}

-

-	private static class TextEnd extends AbstractVexWidgetHandler {

-

-		@Override

-		public void execute(final VexWidget widget) throws ExecutionException {

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

-		}

-

-	}

-

-	private static class SelectPreviousColumn extends AbstractVexWidgetHandler {

-

-		@Override

-		public void execute(final VexWidget widget) throws ExecutionException {

-			widget.moveBy(-1, true);

-		}

-

-	}

-

-	private static class SelectNextColumn extends AbstractVexWidgetHandler {

-

-		@Override

-		public void execute(final VexWidget widget) throws ExecutionException {

-			widget.moveBy(1, true);

-		}

-

-	}

-

-	private static class SelectLineUp extends AbstractVexWidgetHandler {

-

-		@Override

-		public void execute(final VexWidget widget) throws ExecutionException {

-			widget.moveToPreviousLine(true);

-		}

-

-	}

-

-	private static class SelectLineDown extends AbstractVexWidgetHandler {

-

-		@Override

-		public void execute(final VexWidget widget) throws ExecutionException {

-			widget.moveToNextLine(true);

-		}

-

-	}

-

-	private static class SelectPreviousWord extends AbstractVexWidgetHandler {

-

-		@Override

-		public void execute(final VexWidget widget) throws ExecutionException {

-			widget.moveToPreviousWord(true);

-		}

-

-	}

-

-	private static class SelectNextWord extends AbstractVexWidgetHandler {

-

-		@Override

-		public void execute(final VexWidget widget) throws ExecutionException {

-			widget.moveToNextWord(true);

-		}

-

-	}

-

-	private static class SelectLineStart extends AbstractVexWidgetHandler {

-

-		@Override

-		public void execute(final VexWidget widget) throws ExecutionException {

-			widget.moveToLineStart(true);

-		}

-

-	}

-

-	private static class SelectLineEnd extends AbstractVexWidgetHandler {

-

-		@Override

-		public void execute(final VexWidget widget) throws ExecutionException {

-			widget.moveToLineEnd(true);

-		}

-

-	}

-

-	private static class SelectPageUp extends AbstractVexWidgetHandler {

-

-		@Override

-		public void execute(final VexWidget widget) throws ExecutionException {

-			widget.moveToPreviousPage(true);

-		}

-

-	}

-

-	private static class SelectPageDown extends AbstractVexWidgetHandler {

-

-		@Override

-		public void execute(final VexWidget widget) throws ExecutionException {

-			widget.moveToNextPage(true);

-		}

-

-	}

-

-	private static class SelectTextStart extends AbstractVexWidgetHandler {

-

-		@Override

-		public void execute(final VexWidget widget) throws ExecutionException {

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

-		}

-

-	}

-

-	private static class SelectTextEnd extends AbstractVexWidgetHandler {

-

-		@Override

-		public void execute(final VexWidget widget) throws ExecutionException {

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

-		}

-

-	}

-

-	private static class DeletePrevious extends AbstractVexWidgetHandler {

-

-		@Override

-		public void execute(final VexWidget widget) throws ExecutionException {

-			widget.deletePreviousChar();

-		}

-	}

-

-	private static class DeleteNext extends AbstractVexWidgetHandler {

-

-		@Override

-		public void execute(final VexWidget widget) throws ExecutionException {

-			widget.deleteNextChar();

-		}

-

-	}

-

-	private static class DeletePreviousWord extends AbstractVexWidgetHandler {

-

-		@Override

-		public void execute(final VexWidget widget) throws ExecutionException {

-			widget.moveToPreviousWord(true);

-			widget.deleteSelection();

-		}

-

-	}

-

-	private static class DeleteNextWord extends AbstractVexWidgetHandler {

-

-		@Override

-		public void execute(final VexWidget widget) throws ExecutionException {

-			widget.moveToNextWord(true);

-			widget.deleteSelection();

-		}

-

-	}

-

-	private static class DeleteLine extends AbstractVexWidgetHandler {

-

-		@Override

-		public void execute(final VexWidget widget) throws ExecutionException {

-			selectWholeLines(widget);

-			widget.deleteSelection();

-

-		}

-

-	}

-

-	private static class DeleteToBeginningOfLine extends AbstractVexWidgetHandler {

-

-		@Override

-		public void execute(final VexWidget widget) throws ExecutionException {

-			final int selectionLength = widget.getSelectedRange().length();

-			if (selectionLength > 1) {

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

-			}

-			widget.moveToLineStart(true);

-			widget.deleteSelection();

-			if (selectionLength > 1) {

-				widget.moveBy(selectionLength, true);

-			}

-		}

-

-	}

-

-	private static class DeleteToEndOfLine extends AbstractVexWidgetHandler {

-

-		@Override

-		public void execute(final VexWidget widget) throws ExecutionException {

-			if (widget.hasSelection()) {

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

-			}

-			widget.moveToLineEnd(true);

-			widget.deleteSelection();

-		}

-

-	}

-

-	private static class CutLine extends AbstractVexWidgetHandler {

-

-		@Override

-		public void execute(final VexWidget widget) throws ExecutionException {

-			selectWholeLines(widget);

-			widget.cutSelection();

-		}

-

-	}

-

-	private static class CutLineToBeginning extends AbstractVexWidgetHandler {

-

-		@Override

-		public void execute(final VexWidget widget) throws ExecutionException {

-			final int selectionLength = widget.getSelectedRange().length();

-			if (selectionLength > 1) {

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

-			}

-			widget.moveToLineStart(true);

-			widget.cutSelection();

-			if (selectionLength > 1) {

-				widget.moveBy(selectionLength, true);

-			}

-		}

-

-	}

-

-	private static class CutLineToEnd extends AbstractVexWidgetHandler {

-

-		@Override

-		public void execute(final VexWidget widget) throws ExecutionException {

-			if (widget.hasSelection()) {

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

-			}

-			widget.moveToLineEnd(true);

-			widget.cutSelection();

-		}

-

-	}

-

-	private static void selectWholeLines(final VexWidget widget) {

-

-		// no selection?

-		if (!widget.hasSelection()) {

-			widget.moveToLineStart(false);

-			widget.moveToLineEnd(true);

-			return;

-		}

-

-		// remember start

-		final ContentPosition start = widget.getSelectedPositionRange().getStartPosition();

-

-		// calculate end of deletion

-		ContentPosition end = widget.getSelectedPositionRange().getEndPosition();

-		widget.moveTo(end);

-		widget.moveToLineEnd(false);

-		end = widget.getCaretPosition();

-

-		// go to start of deletion

-		widget.moveTo(start);

-		widget.moveToLineStart(false);

-

-		// select and delete

-		widget.moveTo(end, true);

-

-	}

-

-}

+/*******************************************************************************
+ * Copyright (c) 2013 Holger Voormann 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:
+ *     Holger Voormann - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.vex.ui.internal.handlers;
+
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IConfigurationElement;
+import org.eclipse.core.runtime.IExecutableExtension;
+import org.eclipse.core.runtime.IExecutableExtensionFactory;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.vex.core.internal.widget.IDocumentEditor;
+import org.eclipse.vex.core.provisional.dom.ContentPosition;
+import org.eclipse.vex.ui.internal.VexPlugin;
+
+/**
+ * Factory for following command handlers:
+ * <ul>
+ * <li><em>Go to</em> and <em>Select</em>:
+ * <ul>
+ * <li>Previous/Next Column (<em>column</em> in terms of <em>character</em>)</li>
+ * <li>Line Up/Down</li>
+ * <li>Previous/Next Word</li>
+ * <li>Line Start/End</li>
+ * <li>Page Up/Down</li>
+ * <li>Text Start/End (<em>text</em> in terms of <em>document</em>)</li>
+ * </ul>
+ * </li>
+ * <li><em>Delete</em>:
+ * <ul>
+ * <li>Previous/Next (Character)</li>
+ * <li>Previous/Next Word</li>
+ * <li>Line</li>
+ * <li>To Beginning/End of Line</li>
+ * </ul>
+ * </li>
+ * <li><em>Cut</em>:
+ * <ul>
+ * <li>Line</li>
+ * <li>Delete to Beginning/End of Line</li>
+ * </ul>
+ * </li>
+ * </ul>
+ */
+public class TextEditingHandlerFactory implements IExecutableExtensionFactory, IExecutableExtension {
+
+	private String id;
+
+	@Override
+	public Object create() throws CoreException {
+
+		// go to handlers
+		if (PreviousColumn.class.getSimpleName().equals(id)) {
+			return new PreviousColumn();
+		}
+		if (NextColumn.class.getSimpleName().equals(id)) {
+			return new NextColumn();
+		}
+		if (LineUp.class.getSimpleName().equals(id)) {
+			return new LineUp();
+		}
+		if (LineDown.class.getSimpleName().equals(id)) {
+			return new LineDown();
+		}
+		if (PreviousWord.class.getSimpleName().equals(id)) {
+			return new PreviousWord();
+		}
+		if (NextWord.class.getSimpleName().equals(id)) {
+			return new NextWord();
+		}
+		if (LineStart.class.getSimpleName().equals(id)) {
+			return new LineStart();
+		}
+		if (LineEnd.class.getSimpleName().equals(id)) {
+			return new LineEnd();
+		}
+		if (PageUp.class.getSimpleName().equals(id)) {
+			return new PageUp();
+		}
+		if (PageDown.class.getSimpleName().equals(id)) {
+			return new PageDown();
+		}
+		if (TextStart.class.getSimpleName().equals(id)) {
+			return new TextStart();
+		}
+		if (TextEnd.class.getSimpleName().equals(id)) {
+			return new TextEnd();
+		}
+
+		// select handlers
+		if (SelectPreviousColumn.class.getSimpleName().equals(id)) {
+			return new SelectPreviousColumn();
+		}
+		if (SelectNextColumn.class.getSimpleName().equals(id)) {
+			return new SelectNextColumn();
+		}
+		if (SelectLineUp.class.getSimpleName().equals(id)) {
+			return new SelectLineUp();
+		}
+		if (SelectLineDown.class.getSimpleName().equals(id)) {
+			return new SelectLineDown();
+		}
+		if (SelectPreviousWord.class.getSimpleName().equals(id)) {
+			return new SelectPreviousWord();
+		}
+		if (SelectNextWord.class.getSimpleName().equals(id)) {
+			return new SelectNextWord();
+		}
+		if (SelectLineStart.class.getSimpleName().equals(id)) {
+			return new SelectLineStart();
+		}
+		if (SelectLineEnd.class.getSimpleName().equals(id)) {
+			return new SelectLineEnd();
+		}
+		if (SelectPageUp.class.getSimpleName().equals(id)) {
+			return new SelectPageUp();
+		}
+		if (SelectPageDown.class.getSimpleName().equals(id)) {
+			return new SelectPageDown();
+		}
+		if (SelectTextStart.class.getSimpleName().equals(id)) {
+			return new SelectTextStart();
+		}
+		if (SelectTextEnd.class.getSimpleName().equals(id)) {
+			return new SelectTextEnd();
+		}
+
+		// delete handlers
+		if (DeletePrevious.class.getSimpleName().equals(id)) {
+			return new DeletePrevious();
+		}
+		if (DeleteNext.class.getSimpleName().equals(id)) {
+			return new DeleteNext();
+		}
+		if (DeletePreviousWord.class.getSimpleName().equals(id)) {
+			return new DeletePreviousWord();
+		}
+		if (DeleteNextWord.class.getSimpleName().equals(id)) {
+			return new DeleteNextWord();
+		}
+		if (DeleteLine.class.getSimpleName().equals(id)) {
+			return new DeleteLine();
+		}
+		if (DeleteToBeginningOfLine.class.getSimpleName().equals(id)) {
+			return new DeleteToBeginningOfLine();
+		}
+		if (DeleteToEndOfLine.class.getSimpleName().equals(id)) {
+			return new DeleteToEndOfLine();
+		}
+
+		// cut handlers
+		if (CutLine.class.getSimpleName().equals(id)) {
+			return new CutLine();
+		}
+		if (CutLineToBeginning.class.getSimpleName().equals(id)) {
+			return new CutLineToBeginning();
+		}
+		if (CutLineToEnd.class.getSimpleName().equals(id)) {
+			return new CutLineToEnd();
+		}
+
+		// not available
+		throw new CoreException(new Status(IStatus.ERROR, VexPlugin.ID, 0, "Unknown id in data argument for " + getClass(), null)); //$NON-NLS-1$
+	}
+
+	@Override
+	public void setInitializationData(final IConfigurationElement config, final String propertyName, final Object data) throws CoreException {
+		if (data instanceof String) {
+			id = (String) data;
+			return;
+		}
+		throw new CoreException(new Status(IStatus.ERROR, VexPlugin.ID, 0, "Data argument must be a String for " + getClass(), null)); //$NON-NLS-1$
+	}
+
+	private static class PreviousColumn extends AbstractVexWidgetHandler {
+
+		@Override
+		public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+			editor.moveBy(-1);
+		}
+
+	}
+
+	private static class NextColumn extends AbstractVexWidgetHandler {
+
+		@Override
+		public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+			editor.moveBy(1);
+		}
+
+	}
+
+	private static class LineUp extends AbstractVexWidgetHandler {
+
+		@Override
+		public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+			editor.moveToPreviousLine(false);
+		}
+
+	}
+
+	private static class LineDown extends AbstractVexWidgetHandler {
+
+		@Override
+		public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+			editor.moveToNextLine(false);
+		}
+
+	}
+
+	private static class PreviousWord extends AbstractVexWidgetHandler {
+
+		@Override
+		public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+			editor.moveToPreviousWord(false);
+		}
+
+	}
+
+	private static class NextWord extends AbstractVexWidgetHandler {
+
+		@Override
+		public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+			editor.moveToNextWord(false);
+		}
+
+	}
+
+	private static class LineStart extends AbstractVexWidgetHandler {
+
+		@Override
+		public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+			editor.moveToLineStart(false);
+		}
+
+	}
+
+	private static class LineEnd extends AbstractVexWidgetHandler {
+
+		@Override
+		public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+			editor.moveToLineEnd(false);
+		}
+
+	}
+
+	private static class PageUp extends AbstractVexWidgetHandler {
+
+		@Override
+		public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+			editor.moveToPreviousPage(false);
+		}
+
+	}
+
+	private static class PageDown extends AbstractVexWidgetHandler {
+
+		@Override
+		public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+			editor.moveToNextPage(false);
+		}
+
+	}
+
+	private static class TextStart extends AbstractVexWidgetHandler {
+
+		@Override
+		public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+			editor.moveTo(editor.getDocument().getStartPosition());
+		}
+
+	}
+
+	private static class TextEnd extends AbstractVexWidgetHandler {
+
+		@Override
+		public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+			editor.moveTo(editor.getDocument().getEndPosition().moveBy(-1));
+		}
+
+	}
+
+	private static class SelectPreviousColumn extends AbstractVexWidgetHandler {
+
+		@Override
+		public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+			editor.moveBy(-1, true);
+		}
+
+	}
+
+	private static class SelectNextColumn extends AbstractVexWidgetHandler {
+
+		@Override
+		public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+			editor.moveBy(1, true);
+		}
+
+	}
+
+	private static class SelectLineUp extends AbstractVexWidgetHandler {
+
+		@Override
+		public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+			editor.moveToPreviousLine(true);
+		}
+
+	}
+
+	private static class SelectLineDown extends AbstractVexWidgetHandler {
+
+		@Override
+		public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+			editor.moveToNextLine(true);
+		}
+
+	}
+
+	private static class SelectPreviousWord extends AbstractVexWidgetHandler {
+
+		@Override
+		public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+			editor.moveToPreviousWord(true);
+		}
+
+	}
+
+	private static class SelectNextWord extends AbstractVexWidgetHandler {
+
+		@Override
+		public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+			editor.moveToNextWord(true);
+		}
+
+	}
+
+	private static class SelectLineStart extends AbstractVexWidgetHandler {
+
+		@Override
+		public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+			editor.moveToLineStart(true);
+		}
+
+	}
+
+	private static class SelectLineEnd extends AbstractVexWidgetHandler {
+
+		@Override
+		public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+			editor.moveToLineEnd(true);
+		}
+
+	}
+
+	private static class SelectPageUp extends AbstractVexWidgetHandler {
+
+		@Override
+		public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+			editor.moveToPreviousPage(true);
+		}
+
+	}
+
+	private static class SelectPageDown extends AbstractVexWidgetHandler {
+
+		@Override
+		public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+			editor.moveToNextPage(true);
+		}
+
+	}
+
+	private static class SelectTextStart extends AbstractVexWidgetHandler {
+
+		@Override
+		public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+			editor.moveTo(editor.getDocument().getStartPosition(), true);
+		}
+
+	}
+
+	private static class SelectTextEnd extends AbstractVexWidgetHandler {
+
+		@Override
+		public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+			editor.moveTo(editor.getDocument().getEndPosition().moveBy(-1), true);
+		}
+
+	}
+
+	private static class DeletePrevious extends AbstractVexWidgetHandler {
+
+		@Override
+		public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+			editor.deleteBackward();
+		}
+	}
+
+	private static class DeleteNext extends AbstractVexWidgetHandler {
+
+		@Override
+		public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+			editor.deleteForward();
+		}
+
+	}
+
+	private static class DeletePreviousWord extends AbstractVexWidgetHandler {
+
+		@Override
+		public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+			editor.moveToPreviousWord(true);
+			editor.deleteSelection();
+		}
+
+	}
+
+	private static class DeleteNextWord extends AbstractVexWidgetHandler {
+
+		@Override
+		public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+			editor.moveToNextWord(true);
+			editor.deleteSelection();
+		}
+
+	}
+
+	private static class DeleteLine extends AbstractVexWidgetHandler {
+
+		@Override
+		public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+			selectWholeLines(editor);
+			editor.deleteSelection();
+
+		}
+
+	}
+
+	private static class DeleteToBeginningOfLine extends AbstractVexWidgetHandler {
+
+		@Override
+		public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+			final int selectionLength = editor.getSelectedRange().length();
+			if (selectionLength > 1) {
+				editor.moveTo(editor.getSelectedPositionRange().getStartPosition());
+			}
+			editor.moveToLineStart(true);
+			editor.deleteSelection();
+			if (selectionLength > 1) {
+				editor.moveBy(selectionLength, true);
+			}
+		}
+
+	}
+
+	private static class DeleteToEndOfLine extends AbstractVexWidgetHandler {
+
+		@Override
+		public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+			if (editor.hasSelection()) {
+				editor.moveTo(editor.getSelectedPositionRange().getStartPosition());
+			}
+			editor.moveToLineEnd(true);
+			editor.deleteSelection();
+		}
+
+	}
+
+	private static class CutLine extends AbstractVexWidgetHandler {
+
+		@Override
+		public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+			selectWholeLines(editor);
+			editor.cutSelection();
+		}
+
+	}
+
+	private static class CutLineToBeginning extends AbstractVexWidgetHandler {
+
+		@Override
+		public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+			final int selectionLength = editor.getSelectedRange().length();
+			if (selectionLength > 1) {
+				editor.moveTo(editor.getSelectedPositionRange().getStartPosition());
+			}
+			editor.moveToLineStart(true);
+			editor.cutSelection();
+			if (selectionLength > 1) {
+				editor.moveBy(selectionLength, true);
+			}
+		}
+
+	}
+
+	private static class CutLineToEnd extends AbstractVexWidgetHandler {
+
+		@Override
+		public void execute(ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+			if (editor.hasSelection()) {
+				editor.moveTo(editor.getSelectedPositionRange().getStartPosition());
+			}
+			editor.moveToLineEnd(true);
+			editor.cutSelection();
+		}
+
+	}
+
+	private static void selectWholeLines(final IDocumentEditor editor) {
+
+		// no selection?
+		if (!editor.hasSelection()) {
+			editor.moveToLineStart(false);
+			editor.moveToLineEnd(true);
+			return;
+		}
+
+		// remember start
+		final ContentPosition start = editor.getSelectedPositionRange().getStartPosition();
+
+		// calculate end of deletion
+		ContentPosition end = editor.getSelectedPositionRange().getEndPosition();
+		editor.moveTo(end);
+		editor.moveToLineEnd(false);
+		end = editor.getCaretPosition();
+
+		// go to start of deletion
+		editor.moveTo(start);
+		editor.moveToLineStart(false);
+
+		// select and delete
+		editor.moveTo(end, true);
+
+	}
+
+}
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/VexHandlerUtil.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/VexHandlerUtil.java
index 43bb7c4..db1c527 100644
--- a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/VexHandlerUtil.java
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/handlers/VexHandlerUtil.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2004, 2013 John Krasnay and others.
+ * Copyright (c) 2004, 2016 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
@@ -9,6 +9,7 @@
  *     John Krasnay - initial API and implementation
  *     Igor Jacy Lino Campista - Java 5 warnings fixed (bug 311325)
  *     Carsten Hiesserich - use a visitor to create a new table row
+ *     Florian Thienel - introduce IDocumentEditor
  *******************************************************************************/
 package org.eclipse.vex.ui.internal.handlers;
 
@@ -22,14 +23,14 @@
 import org.eclipse.ui.IEditorPart;
 import org.eclipse.ui.IWorkbenchWindow;
 import org.eclipse.ui.handlers.HandlerUtil;
+import org.eclipse.vex.core.internal.core.Rectangle;
 import org.eclipse.vex.core.internal.css.CSS;
 import org.eclipse.vex.core.internal.css.StyleSheet;
 import org.eclipse.vex.core.internal.layout.ElementOrPositionRangeCallback;
 import org.eclipse.vex.core.internal.layout.ElementOrRangeCallback;
 import org.eclipse.vex.core.internal.layout.LayoutUtils;
 import org.eclipse.vex.core.internal.layout.LayoutUtils.ElementOrRange;
-import org.eclipse.vex.core.internal.widget.IVexWidget;
-import org.eclipse.vex.core.internal.widget.swt.VexWidget;
+import org.eclipse.vex.core.internal.widget.IDocumentEditor;
 import org.eclipse.vex.core.provisional.dom.BaseNodeVisitor;
 import org.eclipse.vex.core.provisional.dom.ContentPosition;
 import org.eclipse.vex.core.provisional.dom.ContentPositionRange;
@@ -41,6 +42,7 @@
 import org.eclipse.vex.core.provisional.dom.INode;
 import org.eclipse.vex.core.provisional.dom.IParent;
 import org.eclipse.vex.core.provisional.dom.IProcessingInstruction;
+import org.eclipse.vex.ui.internal.editor.DocumentContextSourceProvider;
 import org.eclipse.vex.ui.internal.editor.VexEditor;
 
 /**
@@ -48,28 +50,34 @@
  */
 public final class VexHandlerUtil {
 
-	public static VexWidget computeWidget(final ExecutionEvent event) throws ExecutionException {
-
+	public static IDocumentEditor getDocumentEditor(final ExecutionEvent event) throws ExecutionException {
 		final IEditorPart activeEditor = HandlerUtil.getActiveEditor(event);
-		assertNotNull(activeEditor);
-
-		VexWidget widget = null;
+		IDocumentEditor documentEditor = null;
 		if (activeEditor instanceof VexEditor) {
-			widget = ((VexEditor) activeEditor).getVexWidget();
+			documentEditor = ((VexEditor) activeEditor).getVexWidget();
 		}
-		assertNotNull(widget);
-		return widget;
+		assertNotNull(documentEditor, "Can not compute document editor.");
+		return documentEditor;
 	}
 
-	public static VexWidget computeWidget(final IWorkbenchWindow window) {
-		final VexEditor editor = computeVexEditor(window);
+	public static IDocumentEditor getDocumentEditor(final IWorkbenchWindow window) {
+		final VexEditor editor = getActiveVexEditor(window);
 		if (editor == null) {
 			return null;
 		}
 		return editor.getVexWidget();
 	}
 
-	public static VexEditor computeVexEditor(final IWorkbenchWindow window) {
+	public static VexEditor getActiveVexEditor(final ExecutionEvent event) throws ExecutionException {
+		final IEditorPart activeEditor = HandlerUtil.getActiveEditor(event);
+
+		if (activeEditor instanceof VexEditor) {
+			return (VexEditor) activeEditor;
+		}
+		return null;
+	}
+
+	public static VexEditor getActiveVexEditor(final IWorkbenchWindow window) {
 		final IEditorPart activeEditor = window.getActivePage().getActiveEditor();
 		if (activeEditor == null) {
 			return null;
@@ -81,9 +89,22 @@
 		return null;
 	}
 
-	private static void assertNotNull(final Object object) throws ExecutionException {
+	public static Rectangle getCaretArea(final ExecutionEvent event) throws ExecutionException {
+		return getVariable(event, DocumentContextSourceProvider.CARET_AREA);
+	}
+
+	public static INode getCurrentNode(final ExecutionEvent event) throws ExecutionException {
+		return getVariable(event, DocumentContextSourceProvider.CURRENT_NODE);
+	}
+
+	@SuppressWarnings("unchecked")
+	private static <T> T getVariable(final ExecutionEvent event, final String name) {
+		return (T) HandlerUtil.getVariable(event, DocumentContextSourceProvider.CARET_AREA);
+	}
+
+	private static void assertNotNull(final Object object, final String message) throws ExecutionException {
 		if (object == null) {
-			throw new ExecutionException("Can not compute VexWidget.");
+			throw new ExecutionException(message);
 		}
 	}
 
@@ -101,27 +122,27 @@
 	 * Duplicate the given table row, inserting a new empty one aove or below it. The new row contains empty children
 	 * corresponding to the given row's children.
 	 *
-	 * @param vexWidget
+	 * @param editor
 	 *            IVexWidget with which we're working
 	 * @param tableRow
 	 *            TableRowBox to be duplicated.
 	 * @param addAbove
 	 *            <code>true</code> to add the new row above the current one
 	 */
-	public static void duplicateTableRow(final IVexWidget vexWidget, final IElement tableRow, final boolean addAbove) {
-		final StyleSheet styleSheet = vexWidget.getStyleSheet();
+	public static void duplicateTableRow(final IDocumentEditor editor, final IElement tableRow, final boolean addAbove) {
+		final StyleSheet styleSheet = editor.getTableModel().getStyleSheet();
 		if (!styleSheet.getStyles(tableRow).getDisplay().equals(CSS.TABLE_ROW)) {
 			return;
 		}
 
 		if (addAbove) {
-			vexWidget.moveTo(tableRow.getStartPosition());
+			editor.moveTo(tableRow.getStartPosition());
 		} else {
-			vexWidget.moveTo(tableRow.getEndPosition().moveBy(1));
+			editor.moveTo(tableRow.getEndPosition().moveBy(1));
 		}
 
 		// Create a new table row
-		final IElement newRow = vexWidget.insertElement(tableRow.getQualifiedName());
+		final IElement newRow = editor.insertElement(tableRow.getQualifiedName());
 
 		// Iterate all direct children and add them to the new row
 		final Iterator<? extends INode> childIterator = tableRow.children().withoutText().iterator();
@@ -129,49 +150,49 @@
 			childIterator.next().accept(new BaseNodeVisitor() {
 				@Override
 				public void visit(final IElement element) {
-					final IElement newElement = vexWidget.insertElement(element.getQualifiedName());
+					final IElement newElement = editor.insertElement(element.getQualifiedName());
 					for (final IAttribute attr : element.getAttributes()) {
 						newElement.setAttribute(attr.getQualifiedName(), attr.getValue());
 					}
-					vexWidget.moveBy(1);
+					editor.moveBy(1);
 				}
 
 				@Override
 				public void visit(final IComment comment) {
 					// Comments are copied with content
-					vexWidget.insertComment();
-					vexWidget.insertText(comment.getText());
-					vexWidget.moveBy(1);
+					editor.insertComment();
+					editor.insertText(comment.getText());
+					editor.moveBy(1);
 				}
 
 				@Override
 				public void visit(final IProcessingInstruction pi) {
 					// Processing instructions are copied with target and  content
-					vexWidget.insertProcessingInstruction(pi.getTarget());
-					vexWidget.insertText(pi.getText());
-					vexWidget.moveBy(1);
+					editor.insertProcessingInstruction(pi.getTarget());
+					editor.insertText(pi.getText());
+					editor.moveBy(1);
 				}
 			});
 		}
 		try {
 			final INode firstTextChild = newRow.childElements().first();
-			vexWidget.moveTo(firstTextChild.getStartPosition());
+			editor.moveTo(firstTextChild.getStartPosition());
 		} catch (final NoSuchElementException ex) {
-			vexWidget.moveTo(newRow.getStartPosition().moveBy(1));
+			editor.moveTo(newRow.getStartPosition().moveBy(1));
 		}
 	}
 
 	/**
 	 * Returns true if the given element or range is at least partially selected.
 	 *
-	 * @param widget
+	 * @param editor
 	 *            IVexWidget being tested.
 	 * @param elementOrRange
 	 *            Element or IntRange being tested.
 	 */
-	public static boolean elementOrRangeIsPartiallySelected(final IVexWidget widget, final Object elementOrRange) {
+	public static boolean elementOrRangeIsPartiallySelected(final IDocumentEditor editor, final Object elementOrRange) {
 		final ContentRange elementContentRange = getInnerRange(elementOrRange);
-		final ContentRange selectedRange = widget.getSelectedRange();
+		final ContentRange selectedRange = editor.getSelectedRange();
 		return elementContentRange.intersects(selectedRange);
 	}
 
@@ -179,17 +200,17 @@
 	 * Returns the zero-based index of the table column containing the current offset. Returns -1 if we are not inside a
 	 * table.
 	 */
-	public static int getCurrentColumnIndex(final IVexWidget vexWidget) {
+	public static int getCurrentColumnIndex(final IDocumentEditor editor) {
 
-		final IElement row = getCurrentTableRow(vexWidget);
+		final IElement row = getCurrentTableRow(editor);
 
 		if (row == null) {
 			return -1;
 		}
 
-		final ContentPosition offset = vexWidget.getCaretPosition();
+		final ContentPosition offset = editor.getCaretPosition();
 		final int[] column = new int[] { -1 };
-		LayoutUtils.iterateTableCells(vexWidget.getStyleSheet(), row, new ElementOrRangeCallback() {
+		LayoutUtils.iterateTableCells(editor.getTableModel().getStyleSheet(), row, new ElementOrRangeCallback() {
 			private int i = 0;
 
 			@Override
@@ -212,12 +233,12 @@
 	/**
 	 * Returns the innermost Element with style table-row containing the caret, or null if no such element exists.
 	 *
-	 * @param vexWidget
+	 * @param editor
 	 *            IVexWidget to use.
 	 */
-	public static IElement getCurrentTableRow(final IVexWidget vexWidget) {
-		final StyleSheet styleSheet = vexWidget.getStyleSheet();
-		IElement element = vexWidget.getCurrentElement();
+	public static IElement getCurrentTableRow(final IDocumentEditor editor) {
+		final StyleSheet styleSheet = editor.getTableModel().getStyleSheet();
+		IElement element = editor.getCurrentElement();
 
 		while (element != null) {
 			if (styleSheet.getStyles(element).getDisplay().equals(CSS.TABLE_ROW)) {
@@ -233,16 +254,16 @@
 	 * Returns the currently selected table rows, or the current row if ther is no selection. If no row can be found,
 	 * returns an empty array.
 	 *
-	 * @param vexWidget
+	 * @param editor
 	 *            IVexWidget to use.
 	 */
-	public static SelectedRows getSelectedTableRows(final IVexWidget vexWidget) {
+	public static SelectedRows getSelectedTableRows(final IDocumentEditor editor) {
 		final SelectedRows selected = new SelectedRows();
 
-		VexHandlerUtil.iterateTableCells(vexWidget, new TableCellCallbackAdapter() {
+		VexHandlerUtil.iterateTableCells(editor, new TableCellCallbackAdapter() {
 			@Override
 			public void startRow(final Object row, final int rowIndex) {
-				if (VexHandlerUtil.elementOrRangeIsPartiallySelected(vexWidget, row)) {
+				if (VexHandlerUtil.elementOrRangeIsPartiallySelected(editor, row)) {
 					if (selected.rows == null) {
 						selected.rows = new ArrayList<Object>();
 					}
@@ -262,11 +283,11 @@
 		return selected;
 	}
 
-	public static void iterateTableCells(final IVexWidget vexWidget, final ITableCellCallback callback) {
+	public static void iterateTableCells(final IDocumentEditor editor, final ITableCellCallback callback) {
 
-		final StyleSheet ss = vexWidget.getStyleSheet();
+		final StyleSheet ss = editor.getTableModel().getStyleSheet();
 
-		iterateTableRows(vexWidget, new ElementOrPositionRangeCallback() {
+		iterateTableRows(editor, new ElementOrPositionRangeCallback() {
 
 			final private int[] rowIndex = { 0 };
 
@@ -331,19 +352,19 @@
 	 * Returns a RowColumnInfo structure containing information about the table containing the caret. Returns null if
 	 * the caret is not currently inside a table.
 	 *
-	 * @param vexWidget
+	 * @param editor
 	 *            IVexWidget to inspect.
 	 */
-	public static RowColumnInfo getRowColumnInfo(final IVexWidget vexWidget) {
+	public static RowColumnInfo getRowColumnInfo(final IDocumentEditor editor) {
 
 		final boolean[] found = new boolean[1];
 		final RowColumnInfo[] rcInfo = new RowColumnInfo[] { new RowColumnInfo() };
-		final ContentPosition position = vexWidget.getCaretPosition();
+		final ContentPosition position = editor.getCaretPosition();
 
 		rcInfo[0].cellIndex = -1;
 		rcInfo[0].rowIndex = -1;
 
-		iterateTableCells(vexWidget, new ITableCellCallback() {
+		iterateTableCells(editor, new ITableCellCallback() {
 
 			private int rowColumnCount;
 
@@ -386,16 +407,16 @@
 	/**
 	 * Iterate over all rows in the table containing the caret.
 	 *
-	 * @param vexWidget
+	 * @param editor
 	 *            IVexWidget to iterate over.
 	 * @param callback
 	 *            Caller-provided callback that this method calls for each row in the current table.
 	 */
-	public static void iterateTableRows(final IVexWidget vexWidget, final ElementOrPositionRangeCallback callback) {
+	public static void iterateTableRows(final IDocumentEditor editor, final ElementOrPositionRangeCallback callback) {
 
-		final StyleSheet ss = vexWidget.getStyleSheet();
-		final IDocument doc = vexWidget.getDocument();
-		final ContentPosition position = vexWidget.getCaretPosition();
+		final StyleSheet ss = editor.getTableModel().getStyleSheet();
+		final IDocument doc = editor.getDocument();
+		final ContentPosition position = editor.getCaretPosition();
 
 		// This may or may not be a table
 		// In any case, it's the element that contains the top-level table
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/namespace/EditNamespacesController.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/namespace/EditNamespacesController.java
index d48f22c..8de1d71 100644
--- a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/namespace/EditNamespacesController.java
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/namespace/EditNamespacesController.java
@@ -1,99 +1,99 @@
-/*******************************************************************************

- * Copyright (c) 2011 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.ui.internal.namespace;

-

-import java.util.ArrayList;

-import java.util.HashSet;

-import java.util.List;

-

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

-import org.eclipse.vex.core.internal.widget.IVexWidget;

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

-

-/**

- * @author Florian Thienel

- */

-public class EditNamespacesController {

-

-	private final IVexWidget widget;

-	private final IElement element;

-

-	private String defaultNamespaceURI;

-

-	private final List<EditableNamespaceDefinition> namespaceDefinitions;

-

-	public EditNamespacesController(final IVexWidget widget) {

-		this.widget = widget;

-		element = widget.getCurrentElement();

-		Assert.isNotNull(element, "There is no current element available. Namespaces can only be edited in elements.");

-		defaultNamespaceURI = getDefaultNamespaceURI(element);

-		namespaceDefinitions = getNamespaceDefinitions(element);

-	}

-

-	private static String getDefaultNamespaceURI(final IElement element) {

-		final String result = element.getDeclaredDefaultNamespaceURI();

-		if (result == null) {

-			return "";

-		}

-		return result;

-	}

-

-	private static List<EditableNamespaceDefinition> getNamespaceDefinitions(final IElement element) {

-		final ArrayList<EditableNamespaceDefinition> result = new ArrayList<EditableNamespaceDefinition>();

-		for (final String prefix : element.getDeclaredNamespacePrefixes()) {

-			result.add(new EditableNamespaceDefinition(prefix, element.getNamespaceURI(prefix)));

-		}

-		return result;

-	}

-

-	public String getDefaultNamespaceURI() {

-		return defaultNamespaceURI;

-	}

-

-	public void setDefaultNamespaceURI(final String defaultNamespaceURI) {

-		this.defaultNamespaceURI = defaultNamespaceURI;

-	}

-

-	public List<EditableNamespaceDefinition> getNamespaceDefinitions() {

-		return namespaceDefinitions;

-	}

-

-	public EditableNamespaceDefinition addNamespaceDefinition() {

-		final EditableNamespaceDefinition result = new EditableNamespaceDefinition();

-		namespaceDefinitions.add(result);

-		return result;

-	}

-

-	public void removeNamespaceDefinition(final EditableNamespaceDefinition namespaceDefinition) {

-		namespaceDefinitions.remove(namespaceDefinition);

-	}

-

-	public void applyToElement() {

-		if (defaultNamespaceURI == null || "".equals(defaultNamespaceURI)) {

-			widget.removeDefaultNamespace();

-		} else {

-			widget.declareDefaultNamespace(defaultNamespaceURI);

-		}

-

-		final HashSet<String> declaredPrefixes = new HashSet<String>();

-		for (final EditableNamespaceDefinition definition : namespaceDefinitions) {

-			widget.declareNamespace(definition.getPrefix(), definition.getUri());

-			declaredPrefixes.add(definition.getPrefix());

-		}

-

-		for (final String prefix : element.getDeclaredNamespacePrefixes()) {

-			if (!declaredPrefixes.contains(prefix)) {

-				widget.removeNamespace(prefix);

-			}

-		}

-	}

-

-}

+/*******************************************************************************
+ * Copyright (c) 2011 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.ui.internal.namespace;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+import org.eclipse.core.runtime.Assert;
+import org.eclipse.vex.core.internal.widget.IDocumentEditor;
+import org.eclipse.vex.core.provisional.dom.IElement;
+
+/**
+ * @author Florian Thienel
+ */
+public class EditNamespacesController {
+
+	private final IDocumentEditor editor;
+	private final IElement element;
+
+	private String defaultNamespaceURI;
+
+	private final List<EditableNamespaceDefinition> namespaceDefinitions;
+
+	public EditNamespacesController(final IDocumentEditor editor) {
+		this.editor = editor;
+		element = editor.getCurrentElement();
+		Assert.isNotNull(element, "There is no current element available. Namespaces can only be edited in elements.");
+		defaultNamespaceURI = getDefaultNamespaceURI(element);
+		namespaceDefinitions = getNamespaceDefinitions(element);
+	}
+
+	private static String getDefaultNamespaceURI(final IElement element) {
+		final String result = element.getDeclaredDefaultNamespaceURI();
+		if (result == null) {
+			return "";
+		}
+		return result;
+	}
+
+	private static List<EditableNamespaceDefinition> getNamespaceDefinitions(final IElement element) {
+		final ArrayList<EditableNamespaceDefinition> result = new ArrayList<EditableNamespaceDefinition>();
+		for (final String prefix : element.getDeclaredNamespacePrefixes()) {
+			result.add(new EditableNamespaceDefinition(prefix, element.getNamespaceURI(prefix)));
+		}
+		return result;
+	}
+
+	public String getDefaultNamespaceURI() {
+		return defaultNamespaceURI;
+	}
+
+	public void setDefaultNamespaceURI(final String defaultNamespaceURI) {
+		this.defaultNamespaceURI = defaultNamespaceURI;
+	}
+
+	public List<EditableNamespaceDefinition> getNamespaceDefinitions() {
+		return namespaceDefinitions;
+	}
+
+	public EditableNamespaceDefinition addNamespaceDefinition() {
+		final EditableNamespaceDefinition result = new EditableNamespaceDefinition();
+		namespaceDefinitions.add(result);
+		return result;
+	}
+
+	public void removeNamespaceDefinition(final EditableNamespaceDefinition namespaceDefinition) {
+		namespaceDefinitions.remove(namespaceDefinition);
+	}
+
+	public void applyToElement() {
+		if (defaultNamespaceURI == null || "".equals(defaultNamespaceURI)) {
+			editor.removeDefaultNamespace();
+		} else {
+			editor.declareDefaultNamespace(defaultNamespaceURI);
+		}
+
+		final HashSet<String> declaredPrefixes = new HashSet<String>();
+		for (final EditableNamespaceDefinition definition : namespaceDefinitions) {
+			editor.declareNamespace(definition.getPrefix(), definition.getUri());
+			declaredPrefixes.add(definition.getPrefix());
+		}
+
+		for (final String prefix : element.getDeclaredNamespacePrefixes()) {
+			if (!declaredPrefixes.contains(prefix)) {
+				editor.removeNamespace(prefix);
+			}
+		}
+	}
+
+}
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/namespace/EditNamespacesHandler.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/namespace/EditNamespacesHandler.java
index c0a2c54..77c0f0f 100644
--- a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/namespace/EditNamespacesHandler.java
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/namespace/EditNamespacesHandler.java
@@ -1,35 +1,41 @@
-/*******************************************************************************

- * Copyright (c) 2011 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.ui.internal.namespace;

-

-import org.eclipse.core.commands.ExecutionException;

-import org.eclipse.jface.window.Window;

-import org.eclipse.vex.core.internal.widget.swt.VexWidget;

-import org.eclipse.vex.ui.internal.handlers.AbstractVexWidgetHandler;

-

-/**

- * @author Florian Thienel

- */

-public class EditNamespacesHandler extends AbstractVexWidgetHandler {

-

-	@Override

-	public void execute(final VexWidget widget) throws ExecutionException {

-		final EditNamespacesController controller = new EditNamespacesController(widget);

-		final EditNamespacesDialog dialog = new EditNamespacesDialog(widget.getShell(), controller);

-		if (dialog.open() == Window.OK) {

-			widget.beginWork();

-			controller.applyToElement();

-			widget.endWork(true);

-			; // TODO maybe we have to refresh something in the widget...

-		}

-	}

-

-}

+/*******************************************************************************
+ * Copyright (c) 2011 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.ui.internal.namespace;
+
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.handlers.HandlerUtil;
+import org.eclipse.vex.core.internal.widget.IDocumentEditor;
+import org.eclipse.vex.ui.internal.handlers.AbstractVexWidgetHandler;
+
+/**
+ * @author Florian Thienel
+ */
+public class EditNamespacesHandler extends AbstractVexWidgetHandler {
+
+	@Override
+	public void execute(final ExecutionEvent event, final IDocumentEditor editor) throws ExecutionException {
+		final Shell shell = HandlerUtil.getActiveShell(event);
+		final EditNamespacesController controller = new EditNamespacesController(editor);
+		final EditNamespacesDialog dialog = new EditNamespacesDialog(shell, controller);
+		if (dialog.open() == Window.OK) {
+			editor.doWork(new Runnable() {
+				@Override
+				public void run() {
+					controller.applyToElement();
+				}
+			}); // TODO maybe we have to refresh something in the widget...
+		}
+	}
+
+}
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/outline/DocumentOutlinePage.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/outline/DocumentOutlinePage.java
index e5c37d1..ab64d90 100644
--- a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/outline/DocumentOutlinePage.java
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/outline/DocumentOutlinePage.java
@@ -37,7 +37,8 @@
 import org.eclipse.ui.part.Page;
 import org.eclipse.ui.views.contentoutline.IContentOutlinePage;
 import org.eclipse.vex.core.internal.css.StyleSheet;
-import org.eclipse.vex.core.internal.widget.swt.VexWidget;
+import org.eclipse.vex.core.internal.widget.IDocumentEditor;
+import org.eclipse.vex.core.internal.widget.swt.BoxWidget;
 import org.eclipse.vex.core.provisional.dom.AttributeChangeEvent;
 import org.eclipse.vex.core.provisional.dom.ContentChangeEvent;
 import org.eclipse.vex.core.provisional.dom.IAttribute;
@@ -66,7 +67,7 @@
 
 	public DocumentOutlinePage(final VexEditor vexEditor) {
 		super();
-		this.vexEditor = vexEditor;
+		editorPart = vexEditor;
 	}
 
 	@Override
@@ -75,10 +76,10 @@
 		composite = new Composite(parent, SWT.NONE);
 		composite.setLayout(new FillLayout());
 
-		vexEditor.addVexEditorListener(vexEditorListener);
-		vexEditor.getEditorSite().getSelectionProvider().addSelectionChangedListener(selectionListener);
+		editorPart.addVexEditorListener(vexEditorListener);
+		editorPart.getEditorSite().getSelectionProvider().addSelectionChangedListener(selectionListener);
 		initToolbarActions();
-		if (vexEditor.isLoaded()) {
+		if (editorPart.isLoaded()) {
 			showTreeViewer();
 		} else {
 			showLabel(Messages.getString("DocumentOutlinePage.loading")); //$NON-NLS-1$
@@ -88,8 +89,8 @@
 
 	@Override
 	public void dispose() {
-		vexEditor.removeVexEditorListener(vexEditorListener);
-		vexEditor.getEditorSite().getSelectionProvider().removeSelectionChangedListener(selectionListener);
+		editorPart.removeVexEditorListener(vexEditorListener);
+		editorPart.getEditorSite().getSelectionProvider().removeSelectionChangedListener(selectionListener);
 		if (filterActionGroup != null) {
 			filterActionGroup.dispose();
 		}
@@ -170,7 +171,7 @@
 	private TreeViewer treeViewer;
 	private OutlineFilterActionGroup filterActionGroup;
 
-	private final VexEditor vexEditor;
+	private final VexEditor editorPart;
 
 	private IOutlineProvider outlineProvider;
 
@@ -209,7 +210,7 @@
 
 		composite.layout();
 
-		final DocumentType doctype = vexEditor.getDocumentType();
+		final DocumentType doctype = editorPart.getDocumentType();
 
 		if (doctype == null) {
 			return;
@@ -238,7 +239,7 @@
 			((IToolBarContributor) outlineProvider).registerToolBarActions(this, getSite().getActionBars());
 		}
 
-		outlineProvider.init(vexEditor);
+		outlineProvider.init(editorPart);
 
 		treeViewer.setContentProvider(outlineProvider.getContentProvider());
 		treeViewer.setLabelProvider(outlineProvider.getLabelProvider());
@@ -247,7 +248,7 @@
 		filterActionGroup.setViewer(treeViewer);
 
 		treeViewer.setUseHashlookup(true);
-		final IDocument document = vexEditor.getVexWidget().getDocument();
+		final IDocument document = editorPart.getVexWidget().getDocument();
 		treeViewer.setInput(document);
 		document.addDocumentListener(documentListener);
 
@@ -270,8 +271,8 @@
 
 		@Override
 		public void selectionChanged(final SelectionChangedEvent event) {
-			if (event.getSource() instanceof VexWidget) {
-				final VexWidget vexWidget = (VexWidget) event.getSource();
+			if (event.getSource() instanceof BoxWidget) {
+				final BoxWidget vexWidget = (BoxWidget) event.getSource();
 				if (vexWidget.isFocusControl() && getTreeViewer() != null) {
 					final INode element = vexWidget.getCurrentNode();
 
@@ -321,13 +322,13 @@
 						lastExpandedElements = null;
 						final INode node = (INode) selected[0].getData();
 						selectedTreeNode = node;
-						final VexWidget vexWidget = vexEditor.getVexWidget();
+						final IDocumentEditor documentEditor = editorPart.getVexWidget();
 
 						// Moving to the end of the element first is a cheap
 						// way to make sure we end up with the
 						// caret at the top of the viewport
-						vexWidget.moveTo(node.getEndPosition());
-						vexWidget.moveTo(node.getStartPosition().moveBy(1), true);
+						documentEditor.moveTo(node.getEndPosition());
+						documentEditor.moveTo(node.getStartPosition().moveBy(1), true);
 					}
 				}
 			}
@@ -346,9 +347,9 @@
 				final TreeItem[] selected = treeViewer.getTree().getSelection();
 				if (selected.length > 0) {
 					final INode node = (INode) selected[0].getData();
-					final VexWidget vexWidget = vexEditor.getVexWidget();
-					vexWidget.moveTo(node.getStartPosition().moveBy(1));
-					vexWidget.setFocus();
+					final IDocumentEditor documentEditor = editorPart.getVexWidget();
+					documentEditor.moveTo(node.getStartPosition().moveBy(1));
+					editorPart.setFocus();
 				}
 			}
 		}
@@ -381,7 +382,7 @@
 			// This cast is save because this event is only fired due to the attribute changes of elements.
 			final IElement parent = (IElement) event.getParent();
 			final IAttribute attr = parent.getAttribute(event.getAttributeName());
-			if (vexEditor.getStyle().getStyleSheet().getStyles(parent).getOutlineContent() == attr) {
+			if (editorPart.getStyle().getStyleSheet().getStyles(parent).getOutlineContent() == attr) {
 				// Parent has to be refreshed, since it uses this attribute as outline content
 				getTreeViewer().refresh(outlineProvider.getOutlineElement(parent));
 			}
@@ -417,7 +418,7 @@
 			} else if (outlineElement instanceof IElement) {
 				// This SHOULD always be the case
 				final IElement parent = ((IElement) outlineElement).getParentElement();
-				if (parent != null && vexEditor.getStyle().getStyleSheet().getStyles(parent).getOutlineContent() == outlineElement) {
+				if (parent != null && editorPart.getStyle().getStyleSheet().getStyles(parent).getOutlineContent() == outlineElement) {
 					// Parent has to be refreshed, since it uses this element as content
 					getTreeViewer().refresh(outlineProvider.getOutlineElement(parent));
 				} else {
@@ -429,7 +430,7 @@
 
 	private void initToolbarActions() {
 
-		final Style style = vexEditor.getStyle();
+		final Style style = editorPart.getStyle();
 		if (style != null) {
 			filterActionGroup = new OutlineFilterActionGroup(style.getStyleSheet());
 		} else {
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/swt/ContentAssist.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/swt/ContentAssist.java
index 6760b94..2923496 100644
--- a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/swt/ContentAssist.java
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/swt/ContentAssist.java
@@ -52,9 +52,10 @@
 import org.eclipse.swt.widgets.Control;
 import org.eclipse.swt.widgets.Display;
 import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
 import org.eclipse.swt.widgets.Text;
 import org.eclipse.vex.core.internal.core.ElementName;
-import org.eclipse.vex.core.internal.widget.swt.VexWidget;
+import org.eclipse.vex.core.internal.widget.IDocumentEditor;
 import org.eclipse.vex.core.provisional.dom.ContentPosition;
 import org.eclipse.vex.core.provisional.dom.IElement;
 import org.eclipse.vex.ui.internal.Icon;
@@ -69,7 +70,7 @@
 
 	private static final String SETTINGS_SECTION = "contentAssistant"; //$NON-NLS-1$
 
-	private final VexWidget vexWidget;
+	private final IDocumentEditor editor;
 	private final AbstractVexAction[] actions;
 	private final boolean autoExecute;
 	private final Point location;
@@ -80,32 +81,36 @@
 	/**
 	 * Constructs a new content assist dialog which can be opened by {@link #open()}.
 	 *
-	 * @param vexWidget
+	 * @param parentShell
+	 *            TODO
+	 * @param editor
 	 *            the vex widget this content assist belongs to
 	 * @param actions
 	 *            list of actions to select from
 	 * @param autoExecute
 	 *            if {@code true} and if there is only one action then {@link #open()} does not show dialog but executes
 	 *            the only action
+	 * @param location
+	 *            TODO
 	 */
-	private ContentAssist(final VexWidget vexWidget, final AbstractVexAction[] actions, final boolean autoExecute) {
-		super(vexWidget.getShell(), SWT.RESIZE, true, // take focus on open
+	private ContentAssist(final Shell parentShell, final IDocumentEditor editor, final AbstractVexAction[] actions, final boolean autoExecute, final Point location) {
+		super(parentShell, SWT.RESIZE, true, // take focus on open
 				false, // persist size
 				false, // persist location
 				false, // show dialog menu
 				false, // show persist actions
 				null, // title
 				null); // footer line
-		this.vexWidget = vexWidget;
+		this.editor = editor;
 		this.actions = actions;
 		this.autoExecute = autoExecute;
-		location = vexWidget.toDisplay(vexWidget.getLocationForContentAssist());
+		this.location = location;
 	}
 
 	@Override
 	public int open() {
 		if (autoExecute && actions.length == 1) {
-			actions[0].execute(vexWidget);
+			actions[0].execute(editor);
 			return Window.OK;
 		}
 		return super.open();
@@ -212,7 +217,7 @@
 		if (selection instanceof StructuredSelection) {
 			final Object first = ((StructuredSelection) selection).getFirstElement();
 			if (first instanceof AbstractVexAction) {
-				((AbstractVexAction) first).execute(vexWidget);
+				((AbstractVexAction) first).execute(editor);
 			}
 		}
 		close();
@@ -283,22 +288,22 @@
 
 	private static abstract class AbstractVexAction {
 
-		private final VexWidget widget;
+		private final IDocumentEditor editor;
 		private final ElementName elementName;
 		private final String text;
 		private final Icon image;
 
-		public AbstractVexAction(final VexWidget widget, final ElementName elementName, final String text, final Icon image) {
-			this.widget = widget;
+		public AbstractVexAction(final IDocumentEditor editor, final ElementName elementName, final String text, final Icon image) {
+			this.editor = editor;
 			this.elementName = elementName;
 			this.text = text;
 			this.image = image;
 		}
 
-		abstract void execute(VexWidget vexWidget);
+		abstract void execute(IDocumentEditor editor);
 
-		public VexWidget getWidget() {
-			return widget;
+		public IDocumentEditor getEditor() {
+			return editor;
 		}
 
 		public ElementName getElementName() {
@@ -318,57 +323,65 @@
 	/**
 	 * Shows the content assist to add a new element.
 	 *
-	 * @param widget
+	 * @param parentShell
+	 *            TODO
+	 * @param editor
 	 *            the VexWidget which hosts the content assist
+	 * @param location
+	 *            TODO
 	 */
-	public static void openAddElementsContentAssist(final VexWidget widget) {
-		final AbstractVexAction[] addActions = computeAddElementsActions(widget);
-		final ContentAssist assist = new ContentAssist(widget, addActions, true);
+	public static void openAddElementsContentAssist(final Shell parentShell, final IDocumentEditor editor, final Point location) {
+		final AbstractVexAction[] addActions = computeAddElementsActions(editor);
+		final ContentAssist assist = new ContentAssist(parentShell, editor, addActions, true, location);
 		assist.open();
 	}
 
 	/**
 	 * Shows the content assist to convert current element.
-	 *
-	 * @param widget
+	 * 
+	 * @param parentShell
+	 *            TODO
+	 * @param editor
 	 *            the VexWidget which hosts the content assist
+	 * @param location
+	 *            TODO
 	 */
-	public static void openQuickFixContentAssist(final VexWidget widget) {
-		final AbstractVexAction[] quickFixActions = computeQuickFixActions(widget);
-		final ContentAssist assist = new ContentAssist(widget, quickFixActions, true);
+	public static void openQuickFixContentAssist(final Shell parentShell, final IDocumentEditor editor, final Point location) {
+		final AbstractVexAction[] quickFixActions = computeQuickFixActions(editor);
+		final ContentAssist assist = new ContentAssist(parentShell, editor, quickFixActions, true, location);
 		assist.open();
 	}
 
-	private static AbstractVexAction[] computeAddElementsActions(final VexWidget widget) {
-		final ElementName[] names = widget.getValidInsertElements();
+	private static AbstractVexAction[] computeAddElementsActions(final IDocumentEditor editor) {
+		final ElementName[] names = editor.getValidInsertElements();
 		final AbstractVexAction[] actions = new AbstractVexAction[names.length];
 		for (int i = 0; i < names.length; i++) {
 			final QualifiedName qualifiedName = names[i].getQualifiedName();
-			actions[i] = new AbstractVexAction(widget, names[i], names[i].toString(), Icon.ELEMENT) {
+			actions[i] = new AbstractVexAction(editor, names[i], names[i].toString(), Icon.ELEMENT) {
 				@Override
-				public void execute(final VexWidget vexWidget) {
-					getWidget().insertElement(qualifiedName);
+				public void execute(final IDocumentEditor editor) {
+					getEditor().insertElement(qualifiedName);
 				}
 			};
 		}
 		return actions;
 	}
 
-	private static AbstractVexAction[] computeQuickFixActions(final VexWidget widget) {
-		final ElementName[] names = widget.getValidMorphElements();
+	private static AbstractVexAction[] computeQuickFixActions(final IDocumentEditor editor) {
+		final ElementName[] names = editor.getValidMorphElements();
 		final AbstractVexAction[] actions = new AbstractVexAction[names.length];
-		final ContentPosition caretPosition = widget.getCaretPosition();
-		final IElement element = widget.getDocument().getElementForInsertionAt(caretPosition.getOffset());
+		final ContentPosition caretPosition = editor.getCaretPosition();
+		final IElement element = editor.getDocument().getElementForInsertionAt(caretPosition.getOffset());
 		final String sourceName = element.getPrefixedName();
 		for (int i = 0; i < names.length; i++) {
 			final QualifiedName qualifiedName = names[i].getQualifiedName();
 			final String message = Messages.getString("command.convertElement.dynamicCommandName"); //$NON-NLS-1$
 			final String text = MessageFormat.format(message, sourceName, names[i]);
 			final Icon icon = Icon.CONVERT;
-			actions[i] = new AbstractVexAction(widget, names[i], text, icon) {
+			actions[i] = new AbstractVexAction(editor, names[i], text, icon) {
 				@Override
-				public void execute(final VexWidget vexWidget) {
-					getWidget().morph(qualifiedName);
+				public void execute(final IDocumentEditor editor) {
+					getEditor().morph(qualifiedName);
 				}
 			};
 		}
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/views/DebugViewPage.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/views/DebugViewPage.java
index 7c4ad1e..8d8adc6 100644
--- a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/views/DebugViewPage.java
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/views/DebugViewPage.java
@@ -11,8 +11,6 @@
 package org.eclipse.vex.ui.internal.views;
 
 import java.lang.reflect.Field;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
 
 import org.eclipse.jface.layout.GridDataFactory;
 import org.eclipse.jface.viewers.ISelectionChangedListener;
@@ -39,13 +37,9 @@
 import org.eclipse.ui.PartInitException;
 import org.eclipse.ui.part.IPageBookViewPage;
 import org.eclipse.ui.part.IPageSite;
-import org.eclipse.vex.core.internal.core.Caret;
 import org.eclipse.vex.core.internal.core.Rectangle;
 import org.eclipse.vex.core.internal.layout.Box;
-import org.eclipse.vex.core.internal.widget.BaseVexWidget;
-import org.eclipse.vex.core.internal.widget.IBoxFilter;
-import org.eclipse.vex.core.internal.widget.IHostComponent;
-import org.eclipse.vex.core.internal.widget.swt.VexWidget;
+import org.eclipse.vex.core.internal.widget.swt.BoxWidget;
 import org.eclipse.vex.core.provisional.dom.ContentPosition;
 import org.eclipse.vex.core.provisional.dom.ContentRange;
 import org.eclipse.vex.core.provisional.dom.IDocument;
@@ -57,7 +51,7 @@
 class DebugViewPage implements IPageBookViewPage {
 
 	public DebugViewPage(final VexEditor vexEditor) {
-		this.vexEditor = vexEditor;
+		editorPart = vexEditor;
 	}
 
 	@Override
@@ -66,22 +60,22 @@
 		composite = new Composite(parent, SWT.NONE);
 		composite.setLayout(new FillLayout());
 
-		if (vexEditor.isLoaded()) {
+		if (editorPart.isLoaded()) {
 			createDebugPanel();
 		} else {
 			loadingLabel = new Label(composite, SWT.NONE);
 			loadingLabel.setText("Loading...");
 		}
 
-		vexEditor.getEditorSite().getSelectionProvider().addSelectionChangedListener(selectionListener);
+		editorPart.getEditorSite().getSelectionProvider().addSelectionChangedListener(selectionListener);
 	}
 
 	@Override
 	public void dispose() {
-		if (vexWidget != null && !vexWidget.isDisposed()) {
-			vexWidget.removeMouseMoveListener(mouseMoveListener);
+		if (documentEditor != null && !documentEditor.isDisposed()) {
+			documentEditor.removeMouseMoveListener(mouseMoveListener);
 		}
-		vexEditor.getEditorSite().getSelectionProvider().removeSelectionChangedListener(selectionListener);
+		editorPart.getEditorSite().getSelectionProvider().removeSelectionChangedListener(selectionListener);
 	}
 
 	@Override
@@ -114,34 +108,34 @@
 	private static final int WIDTH = 3;
 	private static final int HEIGHT = 4;
 
-	private static Field implField;
-	private static Field rootBoxField;
-	private static Field caretField;
-	private static Field hostComponentField;
-	private static Method findInnermostBoxMethod;
-
-	static {
-		try {
-			implField = VexWidget.class.getDeclaredField("impl");
-			implField.setAccessible(true);
-			rootBoxField = BaseVexWidget.class.getDeclaredField("rootBox");
-			rootBoxField.setAccessible(true);
-			caretField = BaseVexWidget.class.getDeclaredField("caret");
-			caretField.setAccessible(true);
-			hostComponentField = BaseVexWidget.class.getDeclaredField("hostComponent");
-			hostComponentField.setAccessible(true);
-			findInnermostBoxMethod = BaseVexWidget.class.getDeclaredMethod("findInnermostBox", IBoxFilter.class);
-			findInnermostBoxMethod.setAccessible(true);
-		} catch (final Exception e) {
-			e.printStackTrace();
-			// TODO: handle exception
-		}
-	}
+	// TODO find this information elsewhere
+	//	private static Field implField;
+	//	private static Field rootBoxField;
+	//	private static Field caretField;
+	//	private static Field hostComponentField;
+	//	private static Method findInnermostBoxMethod;
+	//
+	//	static {
+	//		try {
+	//			implField = VexWidget.class.getDeclaredField("impl");
+	//			implField.setAccessible(true);
+	//			rootBoxField = BaseVexWidget.class.getDeclaredField("rootBox");
+	//			rootBoxField.setAccessible(true);
+	//			caretField = BaseVexWidget.class.getDeclaredField("caret");
+	//			caretField.setAccessible(true);
+	//			hostComponentField = BaseVexWidget.class.getDeclaredField("hostComponent");
+	//			hostComponentField.setAccessible(true);
+	//			findInnermostBoxMethod = BaseVexWidget.class.getDeclaredMethod("findInnermostBox", IBoxFilter.class);
+	//			findInnermostBoxMethod.setAccessible(true);
+	//		} catch (final Exception e) {
+	//			e.printStackTrace();
+	//			// TODO: handle exception
+	//		}
+	//	}
 
 	private IPageSite site;
-	private final VexEditor vexEditor;
-	private VexWidget vexWidget;
-	private BaseVexWidget impl;
+	private final VexEditor editorPart;
+	private BoxWidget documentEditor;
 	private Composite composite;
 
 	private Label loadingLabel;
@@ -166,16 +160,7 @@
 			loadingLabel = null;
 		}
 
-		vexWidget = vexEditor.getVexWidget();
-		try {
-			impl = (BaseVexWidget) implField.get(vexWidget);
-		} catch (final IllegalArgumentException e) {
-			// TODO Auto-generated catch block
-			e.printStackTrace();
-		} catch (final IllegalAccessException e) {
-			// TODO Auto-generated catch block
-			e.printStackTrace();
-		}
+		documentEditor = (BoxWidget) editorPart.getVexWidget();
 
 		final GridLayout layout = new GridLayout();
 		layout.numColumns = 1;
@@ -266,7 +251,7 @@
 
 		composite.layout();
 
-		vexWidget.addMouseMoveListener(mouseMoveListener);
+		documentEditor.addMouseMoveListener(mouseMoveListener);
 
 		repopulate();
 	}
@@ -274,7 +259,7 @@
 	private final ISelectionChangedListener selectionListener = new ISelectionChangedListener() {
 		@Override
 		public void selectionChanged(final SelectionChangedEvent event) {
-			if (vexWidget == null) {
+			if (documentEditor == null) {
 				createDebugPanel();
 			}
 			repopulate();
@@ -307,7 +292,6 @@
 	}
 
 	private final MouseMoveListener mouseMoveListener = new MouseMoveListener() {
-
 		@Override
 		public void mouseMove(final MouseEvent e) {
 			final Rectangle rect = new Rectangle(e.x, e.y, 0, 0);
@@ -315,22 +299,27 @@
 			setItemFromRect(mouseAbsItem, rect);
 			setItemRel(mouseRelItem, viewport, rect);
 		}
-
 	};
 
 	private Rectangle getCaretBounds() {
-		final Caret caret = (Caret) getFieldValue(caretField, impl);
-		return caret.getBounds();
+		// TODO find this information elsewhere
+		//		final Caret caret = (Caret) getFieldValue(caretField, impl);
+		//		return caret.getBounds();
+		return Rectangle.NULL;
 	}
 
 	private Rectangle getRootBoxBounds() {
-		final Box rootBox = (Box) getFieldValue(rootBoxField, impl);
-		return new Rectangle(rootBox.getX(), rootBox.getY(), rootBox.getWidth(), rootBox.getHeight());
+		// TODO find this information elsewhere
+		//		final Box rootBox = (Box) getFieldValue(rootBoxField, impl);
+		//		return new Rectangle(rootBox.getX(), rootBox.getY(), rootBox.getWidth(), rootBox.getHeight());
+		return Rectangle.NULL;
 	}
 
 	private Rectangle getViewport() {
-		final IHostComponent hc = (IHostComponent) getFieldValue(hostComponentField, impl);
-		return hc.getViewport();
+		// TODO find this information elsewhere
+		//		final IHostComponent hc = (IHostComponent) getFieldValue(hostComponentField, impl);
+		//		return hc.getViewport();
+		return Rectangle.NULL;
 	}
 
 	private Object getFieldValue(final Field field, final Object o) {
@@ -345,7 +334,7 @@
 		setItemFromRect(documentItem, getRootBoxBounds());
 		setFromInnermostBox(boxItem, getInnermostBox());
 		final Rectangle viewport = getViewport();
-		caretOffsetItem.setText(1, impl.getCaretPosition().toString());
+		caretOffsetItem.setText(1, documentEditor.getCaretPosition().toString());
 		caretOffsetContentItem.setText(1, getContent());
 		setItemFromRect(viewportItem, viewport);
 		setItemFromRect(caretAbsItem, getCaretBounds());
@@ -362,20 +351,22 @@
 	}
 
 	private Box getInnermostBox() {
-		try {
-			return (Box) findInnermostBoxMethod.invoke(impl, IBoxFilter.TRUE);
-		} catch (final IllegalArgumentException e) {
-			return null;
-		} catch (final IllegalAccessException e) {
-			return null;
-		} catch (final InvocationTargetException e) {
-			return null;
-		}
+		// TODO find this information elsewhere
+		//		try {
+		//			return (Box) findInnermostBoxMethod.invoke(impl, IBoxFilter.TRUE);
+		//		} catch (final IllegalArgumentException e) {
+		//			return null;
+		//		} catch (final IllegalAccessException e) {
+		//			return null;
+		//		} catch (final InvocationTargetException e) {
+		//			return null;
+		//		}
+		return null;
 	}
 
 	private String getContent() {
-		final ContentPosition offset = impl.getCaretPosition();
-		final IDocument doc = impl.getDocument();
+		final ContentPosition offset = documentEditor.getCaretPosition();
+		final IDocument doc = documentEditor.getDocument();
 		final int len = 8;
 
 		final StringBuilder result = new StringBuilder();
@@ -383,10 +374,13 @@
 		final String content = doc.getContent().getRawText(range);
 
 		final int caretIndex = offset.getOffset() - range.getStartOffset();
-
-		result.append(content.substring(0, caretIndex).replace("\0", "#"));
+		if (caretIndex >= 0) {
+			result.append(content.substring(0, caretIndex).replace("\0", "#"));
+		}
 		result.append("|");
-		result.append(content.substring(caretIndex, content.length()).replace("\0", "#"));
+		if (caretIndex < content.length()) {
+			result.append(content.substring(Math.max(0, caretIndex), content.length()).replace("\0", "#"));
+		}
 		return result.toString();
 	}
 
diff --git a/org.eclipse.vex.xhtml/styles/xhtml1-plain.css b/org.eclipse.vex.xhtml/styles/xhtml1-plain.css
index d1a71e6..86ac32d 100644
--- a/org.eclipse.vex.xhtml/styles/xhtml1-plain.css
+++ b/org.eclipse.vex.xhtml/styles/xhtml1-plain.css
@@ -192,7 +192,7 @@
 }
 
 img {
-  background-image: attr(src);
+  content: image(attr(src));
   height: attr(height);
   width: attr(width);
 }