542496: Add a DocumentBuilder for Creole markup

Includes support for:
- Paragraph / Implicit Paragraph blocks
- Basic span types (Italic/Emphasis/Mark/Bold/Strong/Deleted/Underlined)
- Headings
- LineBreaks
- EntityReferences

Change-Id: Ib8e82a6a7385b3f1c132adc881d096ee15dc6de8
Task-Url: https://bugs.eclipse.org/bugs/show_bug.cgi?id=542496
Signed-off-by: Kevin de Vlaming <kevin.devlaming@tasktop.com>
diff --git a/wikitext/core/org.eclipse.mylyn.wikitext.creole/src/main/java/org/eclipse/mylyn/wikitext/creole/internal/CreoleDocumentBuilder.java b/wikitext/core/org.eclipse.mylyn.wikitext.creole/src/main/java/org/eclipse/mylyn/wikitext/creole/internal/CreoleDocumentBuilder.java
new file mode 100644
index 0000000..6e10d72
--- /dev/null
+++ b/wikitext/core/org.eclipse.mylyn.wikitext.creole/src/main/java/org/eclipse/mylyn/wikitext/creole/internal/CreoleDocumentBuilder.java
@@ -0,0 +1,240 @@
+/*******************************************************************************
+ * Copyright (c) 2018 Tasktop Technologies.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Kevin de Vlaming - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.mylyn.wikitext.creole.internal;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.logging.Logger;
+
+import org.eclipse.mylyn.wikitext.creole.CreoleLanguage;
+import org.eclipse.mylyn.wikitext.parser.Attributes;
+import org.eclipse.mylyn.wikitext.parser.builder.AbstractMarkupDocumentBuilder;
+import org.eclipse.mylyn.wikitext.parser.builder.EntityReferences;
+
+/**
+ * a document builder that emits Creole markup
+ *
+ * @author Kevin de Vlaming
+ * @see CreoleLanguageLanguage
+ * @see CreoleLanguage#createDocumentBuilder(Writer)
+ */
+public class CreoleDocumentBuilder extends AbstractMarkupDocumentBuilder {
+
+	private interface CreoleBlock {
+
+		void lineBreak() throws IOException;
+
+	}
+
+	private class ContentBlock extends NewlineDelimitedBlock implements CreoleBlock {
+
+		protected String prefix;
+
+		protected String suffix;
+
+		private final boolean emitWhenEmpty;
+
+		public ContentBlock(BlockType blockType, String prefix, String suffix, int precedingNewlineCount,
+				int trailingNewlineCount, boolean emitWhenEmpty) {
+			super(blockType, precedingNewlineCount, trailingNewlineCount);
+			this.prefix = prefix;
+			this.suffix = suffix;
+			this.emitWhenEmpty = emitWhenEmpty;
+		}
+
+		public ContentBlock(BlockType blockType, String prefix, String suffix, int precedingNewlineCount,
+				int trailingNewlineCount) {
+			this(blockType, prefix, suffix, precedingNewlineCount, trailingNewlineCount, false);
+		}
+
+		public ContentBlock(String prefix, String suffix, int precedingNewlineCount, int trailingNewlineCount) {
+			this(null, prefix, suffix, precedingNewlineCount, trailingNewlineCount);
+		}
+
+		public ContentBlock(String prefix, String suffix) {
+			this(null, prefix, suffix, 0, 0);
+		}
+
+		@Override
+		public void write(int c) throws IOException {
+			CreoleDocumentBuilder.this.emitContent(c);
+		}
+
+		@Override
+		public void write(String s) throws IOException {
+			CreoleDocumentBuilder.this.emitContent(s);
+		}
+
+		@Override
+		public void open() throws IOException {
+			super.open();
+			pushWriter(new StringWriter());
+		}
+
+		@Override
+		public void close() throws IOException {
+			Writer thisContent = popWriter();
+			String content = thisContent.toString();
+			if (emitWhenEmpty || content.length() > 0) {
+				emitContent(content);
+			}
+			super.close();
+		}
+
+		protected void emitContent(final String content) throws IOException {
+			CreoleDocumentBuilder.this.emitContent(prefix);
+			CreoleDocumentBuilder.this.emitContent(content);
+			CreoleDocumentBuilder.this.emitContent(suffix);
+		}
+
+		@Override
+		public void lineBreak() throws IOException {
+			write("\\\\"); //$NON-NLS-1$
+		}
+
+	}
+
+	private class ImplicitParagraphBlock extends ContentBlock {
+
+		ImplicitParagraphBlock() {
+			super(BlockType.PARAGRAPH, "", "", 2, 2); //$NON-NLS-1$ //$NON-NLS-2$
+		}
+
+		@Override
+		protected boolean isImplicitBlock() {
+			return true;
+		}
+
+	}
+
+	public CreoleDocumentBuilder(Writer out) {
+		super(out);
+	}
+
+	@Override
+	protected Block computeBlock(BlockType type, Attributes attributes) {
+		switch (type) {
+		case PARAGRAPH:
+			return new ContentBlock(type, "", "", 2, 2); //$NON-NLS-1$ //$NON-NLS-2$
+		default:
+			Logger.getLogger(getClass().getName()).warning("Unexpected block type: " + type); //$NON-NLS-1$
+			return new ContentBlock(type, "", "", 2, 2); //$NON-NLS-1$ //$NON-NLS-2$
+		}
+	}
+
+	@Override
+	protected Block computeSpan(SpanType type, Attributes attributes) {
+		switch (type) {
+		case ITALIC:
+		case EMPHASIS:
+		case MARK:
+			return new ContentBlock("//", "//"); //$NON-NLS-1$ //$NON-NLS-2$
+		case BOLD:
+		case STRONG:
+			return new ContentBlock("**", "**"); //$NON-NLS-1$ //$NON-NLS-2$
+		case DELETED:
+			return new ContentBlock("--", "--"); //$NON-NLS-1$ //$NON-NLS-2$
+		case UNDERLINED:
+			return new ContentBlock("__", "__"); //$NON-NLS-1$ //$NON-NLS-2$
+		default:
+			Logger.getLogger(getClass().getName()).warning("Unexpected span type: " + type); //$NON-NLS-1$
+			return new ContentBlock("", "", 2, 2); //$NON-NLS-1$ //$NON-NLS-2$
+		}
+	}
+
+	@Override
+	protected Block computeHeading(int level, Attributes attributes) {
+		return new ContentBlock(computePrefix('=', level) + " ", "", 1, 2); //$NON-NLS-1$
+
+	}
+
+	@Override
+	public void characters(String text) {
+		String escapedText = escapeTilde(text);
+		assertOpenBlock();
+		try {
+			currentBlock.write(escapedText);
+		} catch (IOException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	private String escapeTilde(String text) {
+		return text.replace("~", "&tilde;");
+	}
+
+	@Override
+	public void entityReference(String entity) {
+		assertOpenBlock();
+		String literal = EntityReferences.instance().equivalentString(entity);
+		if (literal == null) {
+			literal = "&" + entity + ";"; //$NON-NLS-1$//$NON-NLS-2$
+		}
+		try {
+			currentBlock.write(literal);
+		} catch (IOException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	@Override
+	public void image(Attributes attributes, String url) {
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public void link(Attributes attributes, String hrefOrHashName, String text) {
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public void imageLink(Attributes linkAttributes, Attributes imageAttributes, String href, String imageUrl) {
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public void acronym(String text, String definition) {
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public void lineBreak() {
+		assertOpenBlock();
+		try {
+			if (currentBlock instanceof CreoleBlock) {
+				((CreoleBlock) currentBlock).lineBreak();
+			}
+		} catch (IOException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	@Override
+	public void horizontalRule() {
+		assertOpenBlock();
+		String horizontalRule = "\n----\n"; //$NON-NLS-1$
+		try {
+			currentBlock.write(horizontalRule);
+		} catch (IOException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	@Override
+	protected Block createImplicitParagraphBlock() {
+		return new ImplicitParagraphBlock();
+	}
+
+}
diff --git a/wikitext/core/org.eclipse.mylyn.wikitext.creole/src/test/java/org/eclipse/mylyn/internal/wikitext/creole/tests/documentbuilder/AbstractCreoleDocumentBuilderTest.java b/wikitext/core/org.eclipse.mylyn.wikitext.creole/src/test/java/org/eclipse/mylyn/internal/wikitext/creole/tests/documentbuilder/AbstractCreoleDocumentBuilderTest.java
new file mode 100644
index 0000000..42212d6
--- /dev/null
+++ b/wikitext/core/org.eclipse.mylyn.wikitext.creole/src/test/java/org/eclipse/mylyn/internal/wikitext/creole/tests/documentbuilder/AbstractCreoleDocumentBuilderTest.java
@@ -0,0 +1,44 @@
+/*******************************************************************************
+ * Copyright (c) 2018 Tasktop Technologies.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Kevin de Vlaming - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.mylyn.internal.wikitext.creole.tests.documentbuilder;
+
+import java.io.StringWriter;
+
+import org.eclipse.mylyn.wikitext.creole.internal.CreoleDocumentBuilder;
+import org.eclipse.mylyn.wikitext.parser.DocumentBuilder;
+
+import junit.framework.TestCase;
+
+/**
+ * @author Kevin de Vlaming
+ */
+public abstract class AbstractCreoleDocumentBuilderTest extends TestCase {
+
+	protected DocumentBuilder builder;
+
+	protected StringWriter out;
+
+	@Override
+	protected void setUp() throws Exception {
+		out = new StringWriter();
+		builder = new CreoleDocumentBuilder(out);
+	}
+
+	protected void assertMarkup(String expected) {
+		String markup = out.toString();
+
+		assertEquals(expected, markup);
+	}
+
+}
\ No newline at end of file
diff --git a/wikitext/core/org.eclipse.mylyn.wikitext.creole/src/test/java/org/eclipse/mylyn/internal/wikitext/creole/tests/documentbuilder/CreoleDocumentBuilderBlockTest.java b/wikitext/core/org.eclipse.mylyn.wikitext.creole/src/test/java/org/eclipse/mylyn/internal/wikitext/creole/tests/documentbuilder/CreoleDocumentBuilderBlockTest.java
new file mode 100644
index 0000000..4ac6299
--- /dev/null
+++ b/wikitext/core/org.eclipse.mylyn.wikitext.creole/src/test/java/org/eclipse/mylyn/internal/wikitext/creole/tests/documentbuilder/CreoleDocumentBuilderBlockTest.java
@@ -0,0 +1,129 @@
+/*******************************************************************************
+ * Copyright (c) 2018 Tasktop Technologies.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Kevin de Vlaming - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.mylyn.internal.wikitext.creole.tests.documentbuilder;
+
+import org.eclipse.mylyn.wikitext.parser.Attributes;
+import org.eclipse.mylyn.wikitext.parser.DocumentBuilder.BlockType;
+import org.eclipse.mylyn.wikitext.parser.DocumentBuilder.SpanType;
+
+/**
+ * @see http://www.wikicreole.org/wiki/Elements
+ * @author Kevin de Vlaming
+ */
+public class CreoleDocumentBuilderBlockTest extends AbstractCreoleDocumentBuilderTest {
+
+	public void testParagraph() {
+		builder.beginDocument();
+		builder.beginBlock(BlockType.PARAGRAPH, new Attributes());
+		builder.characters("A paragraph ends when a blank line begins!");
+		builder.endBlock();
+		builder.endDocument();
+		assertMarkup("A paragraph ends when a blank line begins!\n\n");
+	}
+
+	public void testParagraphConsecutive() {
+		builder.beginDocument();
+		builder.beginBlock(BlockType.PARAGRAPH, new Attributes());
+		builder.characters("Paragraph 1");
+		builder.endBlock();
+		builder.beginBlock(BlockType.PARAGRAPH, new Attributes());
+		builder.characters("Paragraph 2");
+		builder.endBlock();
+		builder.endDocument();
+		assertMarkup("Paragraph 1\n\nParagraph 2\n\n");
+	}
+
+	public void testParagraphWithStrongEmphasis() {
+		builder.beginDocument();
+		builder.beginBlock(BlockType.PARAGRAPH, new Attributes());
+		builder.characters("some ");
+		builder.beginSpan(SpanType.STRONG, new Attributes());
+		builder.characters("strong");
+		builder.endSpan();
+		builder.characters(" and ");
+		builder.beginSpan(SpanType.EMPHASIS, new Attributes());
+		builder.characters("emphasis");
+		builder.endSpan();
+		builder.endBlock();
+		builder.endDocument();
+		assertMarkup("some **strong** and //emphasis//\n\n");
+	}
+
+	public void testImplicitParagraph() {
+		builder.beginDocument();
+		builder.characters("text1");
+		builder.beginBlock(BlockType.PARAGRAPH, new Attributes());
+		builder.characters("text2");
+		builder.endBlock();
+		builder.characters("text3");
+		builder.endDocument();
+		assertMarkup("text1\n\ntext2\n\ntext3\n\n");
+	}
+
+	public void testSpanImplicitParagraph() {
+		builder.beginDocument();
+		builder.beginBlock(BlockType.PARAGRAPH, new Attributes());
+		builder.characters("Paragraph");
+		builder.endBlock();
+		builder.beginSpan(SpanType.ITALIC, new Attributes());
+		builder.characters("Implicit");
+		builder.endSpan();
+		builder.characters(" paragraph");
+		builder.endDocument();
+		assertMarkup("Paragraph\n\n//Implicit// paragraph\n\n");
+	}
+
+	public void testImplicitParagraphWithSpan() {
+		builder.beginDocument();
+		builder.beginSpan(SpanType.BOLD, new Attributes());
+		builder.characters("text1");
+		builder.endSpan();
+		builder.beginBlock(BlockType.PARAGRAPH, new Attributes());
+		builder.characters("text2");
+		builder.endBlock();
+		builder.endDocument();
+		assertMarkup("**text1**\n\ntext2\n\n");
+	}
+
+	public void testSpanOpensImplicitParagraph() {
+		builder.beginDocument();
+		builder.beginBlock(BlockType.PARAGRAPH, new Attributes());
+		builder.characters("text");
+		builder.endBlock();
+		builder.beginSpan(SpanType.BOLD, new Attributes());
+		builder.characters("bold");
+		builder.endSpan();
+		builder.characters(" text");
+		builder.endDocument();
+		assertMarkup("text\n\n**bold** text\n\n");
+	}
+
+	public void testEmptyBlock() {
+		builder.beginDocument();
+		builder.beginBlock(BlockType.PARAGRAPH, new Attributes());
+		builder.endBlock();
+		builder.endDocument();
+		assertMarkup("");
+	}
+
+	public void testUnsupportedBlock() {
+		builder.beginDocument();
+		builder.beginBlock(BlockType.FOOTNOTE, new Attributes());
+		builder.characters("unsupported");
+		builder.endBlock();
+		builder.endDocument();
+		assertMarkup("unsupported\n\n");
+	}
+
+}
\ No newline at end of file
diff --git a/wikitext/core/org.eclipse.mylyn.wikitext.creole/src/test/java/org/eclipse/mylyn/internal/wikitext/creole/tests/documentbuilder/CreoleDocumentBuilderSpanTest.java b/wikitext/core/org.eclipse.mylyn.wikitext.creole/src/test/java/org/eclipse/mylyn/internal/wikitext/creole/tests/documentbuilder/CreoleDocumentBuilderSpanTest.java
new file mode 100644
index 0000000..554b9b4
--- /dev/null
+++ b/wikitext/core/org.eclipse.mylyn.wikitext.creole/src/test/java/org/eclipse/mylyn/internal/wikitext/creole/tests/documentbuilder/CreoleDocumentBuilderSpanTest.java
@@ -0,0 +1,107 @@
+/*******************************************************************************
+ * Copyright (c) 2018 Tasktop Technologies.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Kevin de Vlaming - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.mylyn.internal.wikitext.creole.tests.documentbuilder;
+
+import org.eclipse.mylyn.wikitext.parser.Attributes;
+import org.eclipse.mylyn.wikitext.parser.DocumentBuilder.SpanType;
+
+/**
+ * @see http://www.wikicreole.org/wiki/Elements
+ * @author Kevin de Vlaming
+ */
+public class CreoleDocumentBuilderSpanTest extends AbstractCreoleDocumentBuilderTest {
+
+	public void testItalic() {
+		builder.beginDocument();
+		builder.beginSpan(SpanType.ITALIC, new Attributes());
+		builder.characters("italic");
+		builder.endSpan();
+		builder.endDocument();
+		assertMarkup("//italic//\n\n");
+	}
+
+	public void testEmphasis() {
+		builder.beginDocument();
+		builder.beginSpan(SpanType.EMPHASIS, new Attributes());
+		builder.characters("emphasis");
+		builder.endSpan();
+		builder.endDocument();
+		assertMarkup("//emphasis//\n\n");
+	}
+
+	public void testMark() {
+		builder.beginDocument();
+		builder.beginSpan(SpanType.MARK, new Attributes());
+		builder.characters("mark");
+		builder.endSpan();
+		builder.endDocument();
+		assertMarkup("//mark//\n\n");
+	}
+
+	public void testBold() {
+		builder.beginDocument();
+		builder.beginSpan(SpanType.BOLD, new Attributes());
+		builder.characters("bold");
+		builder.endSpan();
+		builder.endDocument();
+		assertMarkup("**bold**\n\n");
+	}
+
+	public void testStrong() {
+		builder.beginDocument();
+		builder.beginSpan(SpanType.STRONG, new Attributes());
+		builder.characters("strong");
+		builder.endSpan();
+		builder.endDocument();
+		assertMarkup("**strong**\n\n");
+	}
+
+	public void testDeleted() {
+		builder.beginDocument();
+		builder.beginSpan(SpanType.DELETED, new Attributes());
+		builder.characters("deleted");
+		builder.endSpan();
+		builder.endDocument();
+		assertMarkup("--deleted--\n\n");
+	}
+
+	public void testUnderlined() {
+		builder.beginDocument();
+		builder.beginSpan(SpanType.UNDERLINED, new Attributes());
+		builder.characters("underlined");
+		builder.endSpan();
+		builder.endDocument();
+		assertMarkup("__underlined__\n\n");
+	}
+
+	public void testEmptySpan() {
+		builder.beginDocument();
+		builder.characters("prefix");
+		builder.beginSpan(SpanType.BOLD, new Attributes());
+		builder.endSpan();
+		builder.characters(" suffix");
+		builder.endDocument();
+		assertMarkup("prefix suffix\n\n");
+	}
+
+	public void testUnsupportedSpan() {
+		builder.beginDocument();
+		builder.beginSpan(SpanType.INSERTED, new Attributes());
+		builder.characters("unsupported");
+		builder.endSpan();
+		builder.endDocument();
+		assertMarkup("unsupported\n\n");
+	}
+
+}
\ No newline at end of file
diff --git a/wikitext/core/org.eclipse.mylyn.wikitext.creole/src/test/java/org/eclipse/mylyn/internal/wikitext/creole/tests/documentbuilder/CreoleDocumentBuilderTest.java b/wikitext/core/org.eclipse.mylyn.wikitext.creole/src/test/java/org/eclipse/mylyn/internal/wikitext/creole/tests/documentbuilder/CreoleDocumentBuilderTest.java
new file mode 100644
index 0000000..752ce4c
--- /dev/null
+++ b/wikitext/core/org.eclipse.mylyn.wikitext.creole/src/test/java/org/eclipse/mylyn/internal/wikitext/creole/tests/documentbuilder/CreoleDocumentBuilderTest.java
@@ -0,0 +1,108 @@
+/*******************************************************************************
+ * Copyright (c) 2018 Tasktop Technologies.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Kevin de Vlaming - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.mylyn.internal.wikitext.creole.tests.documentbuilder;
+
+import org.eclipse.mylyn.wikitext.parser.Attributes;
+import org.eclipse.mylyn.wikitext.parser.DocumentBuilder.BlockType;
+
+/**
+ * @see http://www.wikicreole.org/wiki/Elements
+ * @author Kevin de Vlaming
+ */
+public class CreoleDocumentBuilderTest extends AbstractCreoleDocumentBuilderTest {
+
+	public void testLineBreak() {
+		builder.beginDocument();
+		builder.beginBlock(BlockType.PARAGRAPH, new Attributes());
+		builder.characters("line");
+		builder.lineBreak();
+		builder.characters("break");
+		builder.endBlock();
+		builder.endDocument();
+		assertMarkup("line\\\\break\n\n");
+	}
+
+	public void testLineBreakImplicitParagraph() {
+		builder.beginDocument();
+		builder.characters("line");
+		builder.lineBreak();
+		builder.characters("break");
+		builder.endDocument();
+		assertMarkup("line\\\\break\n\n");
+	}
+
+	public void testHeadings() {
+		builder.beginDocument();
+		builder.beginHeading(1, new Attributes());
+		builder.characters("This is an H1");
+		builder.endHeading();
+		builder.beginHeading(2, new Attributes());
+		builder.characters("This is an H2");
+		builder.endHeading();
+		builder.beginHeading(6, new Attributes());
+		builder.characters("This is an H6");
+		builder.endHeading();
+		builder.endDocument();
+		assertMarkup("= This is an H1\n\n== This is an H2\n\n====== This is an H6\n\n");
+	}
+
+	public void testHorizontalRule() {
+		builder.beginDocument();
+		builder.beginBlock(BlockType.PARAGRAPH, new Attributes());
+		builder.characters("horizontal");
+		builder.horizontalRule();
+		builder.characters("rule");
+		builder.endBlock();
+		builder.endDocument();
+		assertMarkup("horizontal\n----\nrule\n\n");
+	}
+
+	public void testEscapedTilde() {
+		builder.beginDocument();
+		builder.beginBlock(BlockType.PARAGRAPH, new Attributes());
+		builder.characters("this ~ is interpreted as an escape");
+		builder.endBlock();
+		builder.endDocument();
+		assertMarkup("this &tilde; is interpreted as an escape\n\n");
+	}
+
+	public void testEntityReference() {
+		builder.beginDocument();
+		builder.beginBlock(BlockType.PARAGRAPH, new Attributes());
+		builder.characters("5 ");
+		builder.entityReference("gt");
+		builder.characters(" 4");
+		builder.endBlock();
+		builder.endDocument();
+		assertMarkup("5 > 4\n\n");
+	}
+
+	public void testEntityReferenceImplicitParagraph() {
+		builder.beginDocument();
+		builder.characters("4 ");
+		builder.entityReference("lt");
+		builder.characters(" 5");
+		builder.endDocument();
+		assertMarkup("4 < 5\n\n");
+	}
+
+	public void testUnknownEntityReference() {
+		builder.beginDocument();
+		builder.entityReference("unknown");
+		builder.characters(" reference");
+		builder.endDocument();
+		assertMarkup("&unknown; reference\n\n");
+	}
+
+}
\ No newline at end of file