540235: ExtendedQuoteBlock supports nested blocks over multiple lines

* Override the three functions necessary for nested block supported as
specified in the Block javadocs: beginNesting(), findClosedOffset(), and
canResume(). This way we can leverage the logic within
AbstractMarkupLanguage that manages the state of nested blocks.

* Further in order for AbstractMarkupLanguage to supported
ExtendedQuoteBlock and its nested blocks it needs to know what the
offset is for the current line being processed. Currently the abstract
function in AbstractConfluenceDelimitedBlock assumes the whole line
being processed and has a void return. This is not the case so we should
return an offset when calling handleBlockContent().

* Added tests for the transformation of Confluence markup to HTML

* Added tests for the transformation of HTML document building blocks to
Confluence markup

https://bugs.eclipse.org/bugs/show_bug.cgi?id=540235
Change-Id: Ic11a7605fc0bbca3c97a6e432a4036834faafd33
Signed-off-by: Ryan Nosworthy <ryan.nosworthy@tasktop.com>
diff --git a/wikitext/core/org.eclipse.mylyn.wikitext.confluence/src/main/java/org/eclipse/mylyn/wikitext/confluence/internal/block/AbstractConfluenceDelimitedBlock.java b/wikitext/core/org.eclipse.mylyn.wikitext.confluence/src/main/java/org/eclipse/mylyn/wikitext/confluence/internal/block/AbstractConfluenceDelimitedBlock.java
index 21c347f..b6cfbf1 100644
--- a/wikitext/core/org.eclipse.mylyn.wikitext.confluence/src/main/java/org/eclipse/mylyn/wikitext/confluence/internal/block/AbstractConfluenceDelimitedBlock.java
+++ b/wikitext/core/org.eclipse.mylyn.wikitext.confluence/src/main/java/org/eclipse/mylyn/wikitext/confluence/internal/block/AbstractConfluenceDelimitedBlock.java
@@ -40,38 +40,57 @@
 			beginBlock();
 		}
 
-		int end = line.length();
-		int segmentEnd = end;
+		int endOfContent = line.length();
+		int segmentEnd = endOfContent;
 		boolean terminating = false;
 
-		if (offset < end) {
+		if (offset < endOfContent) {
 			Matcher endMatcher = endPattern.matcher(line);
 			if (blockLineCount == 0) {
-				endMatcher.region(offset, end);
+				endMatcher.region(offset, endOfContent);
 			}
 			if (endMatcher.find()) {
 				terminating = true;
-				end = endMatcher.start(2);
+				endOfContent = endMatcher.start(2);
 				segmentEnd = endMatcher.start(1);
 			}
 		}
 
-		if (end < line.length()) {
-			state.setLineSegmentEndOffset(end);
+		if (endOfContent < line.length()) {
+			state.setLineSegmentEndOffset(endOfContent);
 		}
 
 		++blockLineCount;
 
 		final String content = line.substring(offset, segmentEnd);
-		handleBlockContent(content);
+		int contentOffset = handleBlockContent(content);
 
 		if (terminating) {
 			setClosed(true);
 		}
-		return end == line.length() ? -1 : end;
+
+		return finalOffset(line.length(), endOfContent, contentOffset);
 	}
 
-	protected abstract void handleBlockContent(String content);
+	private int finalOffset(int lineLength, int endOfContent, int contentOffset) {
+		int finalOffset = contentOffset;
+		if (contentOffset == lineLength) {
+			finalOffset = -1;
+		} else if (endOfContent != lineLength) {
+			finalOffset = endOfContent;
+		}
+		return finalOffset;
+	}
+
+	/**
+	 * Process the given line of markup starting at the provided offset.
+	 *
+	 * @param line
+	 *            the markup line to process
+	 * @return a non-negative integer to indicate that processing of the block completed before the end of the line, or
+	 *         -1 if the entire line was processed.
+	 */
+	protected abstract int handleBlockContent(String content);
 
 	protected abstract void beginBlock();
 
diff --git a/wikitext/core/org.eclipse.mylyn.wikitext.confluence/src/main/java/org/eclipse/mylyn/wikitext/confluence/internal/block/CodeBlock.java b/wikitext/core/org.eclipse.mylyn.wikitext.confluence/src/main/java/org/eclipse/mylyn/wikitext/confluence/internal/block/CodeBlock.java
index ce6cd31..ff824b9 100644
--- a/wikitext/core/org.eclipse.mylyn.wikitext.confluence/src/main/java/org/eclipse/mylyn/wikitext/confluence/internal/block/CodeBlock.java
+++ b/wikitext/core/org.eclipse.mylyn.wikitext.confluence/src/main/java/org/eclipse/mylyn/wikitext/confluence/internal/block/CodeBlock.java
@@ -44,15 +44,16 @@
 	}
 
 	@Override
-	protected void handleBlockContent(String content) {
+	protected int handleBlockContent(String content) {
 		builder.characters(content);
 		builder.characters("\n"); //$NON-NLS-1$
+		return -1;
 	}
 
 	@Override
 	protected void endBlock() {
 		if (title != null) {
-			builder.endBlock(); // panel	
+			builder.endBlock(); // panel
 		}
 		builder.endBlock(); // code
 	}
diff --git a/wikitext/core/org.eclipse.mylyn.wikitext.confluence/src/main/java/org/eclipse/mylyn/wikitext/confluence/internal/block/ExtendedPreformattedBlock.java b/wikitext/core/org.eclipse.mylyn.wikitext.confluence/src/main/java/org/eclipse/mylyn/wikitext/confluence/internal/block/ExtendedPreformattedBlock.java
index 1528253..9eef784 100644
--- a/wikitext/core/org.eclipse.mylyn.wikitext.confluence/src/main/java/org/eclipse/mylyn/wikitext/confluence/internal/block/ExtendedPreformattedBlock.java
+++ b/wikitext/core/org.eclipse.mylyn.wikitext.confluence/src/main/java/org/eclipse/mylyn/wikitext/confluence/internal/block/ExtendedPreformattedBlock.java
@@ -18,7 +18,7 @@
 /**
  * quoted text block, matches blocks that start with <code>{noformat}</code>. Creates an extended block type of
  * {@link ParagraphBlock paragraph}.
- * 
+ *
  * @author David Green
  */
 public class ExtendedPreformattedBlock extends AbstractConfluenceDelimitedBlock {
@@ -39,13 +39,14 @@
 	}
 
 	@Override
-	protected void handleBlockContent(String content) {
+	protected int handleBlockContent(String content) {
 		if (content.length() > 0) {
 			builder.characters(content);
 		} else if (blockLineCount == 1) {
-			return;
+			return -1;
 		}
 		builder.characters("\n"); //$NON-NLS-1$
+		return -1;
 	}
 
 	@Override
diff --git a/wikitext/core/org.eclipse.mylyn.wikitext.confluence/src/main/java/org/eclipse/mylyn/wikitext/confluence/internal/block/ExtendedQuoteBlock.java b/wikitext/core/org.eclipse.mylyn.wikitext.confluence/src/main/java/org/eclipse/mylyn/wikitext/confluence/internal/block/ExtendedQuoteBlock.java
index 041af81..9f7cfcc 100644
--- a/wikitext/core/org.eclipse.mylyn.wikitext.confluence/src/main/java/org/eclipse/mylyn/wikitext/confluence/internal/block/ExtendedQuoteBlock.java
+++ b/wikitext/core/org.eclipse.mylyn.wikitext.confluence/src/main/java/org/eclipse/mylyn/wikitext/confluence/internal/block/ExtendedQuoteBlock.java
@@ -20,7 +20,7 @@
 /**
  * quoted text block, matches blocks that start with <code>{quote}</code>. Creates an extended block type of
  * {@link ParagraphBlock paragraph}.
- * 
+ *
  * @author David Green
  */
 public class ExtendedQuoteBlock extends AbstractConfluenceDelimitedBlock {
@@ -64,7 +64,7 @@
 	}
 
 	@Override
-	protected void handleBlockContent(String content) {
+	protected int handleBlockContent(String content) {
 		if (nestedBlock == null) {
 			ConfluenceLanguage markupLanguage = (ConfluenceLanguage) getMarkupLanguage();
 			for (Block block : markupLanguage.getNestedBlocks()) {
@@ -87,22 +87,19 @@
 				nestedBlock = null;
 			}
 			if (lineOffset < content.length() && lineOffset >= 0) {
-				if (nestedBlock != null) {
-					throw new IllegalStateException("if a block does not fully process a line then it must be closed"); //$NON-NLS-1$
-				}
-				content = content.substring(lineOffset);
+				return lineOffset;
 			} else {
-				return;
+				return -1;
 			}
 		}
 		if (blockLineCount == 1 && content.length() == 0) {
-			return;
+			return -1;
 		}
 		if (blockLineCount > 1 && paraOpen && getMarkupLanguage().isEmptyLine(content)) {
 			builder.endBlock(); // para
 			paraOpen = false;
 			paraLine = 0;
-			return;
+			return -1;
 		}
 		if (!paraOpen) {
 			builder.beginBlock(BlockType.PARAGRAPH, new Attributes());
@@ -113,7 +110,31 @@
 		}
 		++paraLine;
 		getMarkupLanguage().emitMarkupLine(getParser(), state, content, 0);
+		return -1;
+	}
 
+	@Override
+	public int findCloseOffset(String line, int lineOffset) {
+		if (nestedBlock == null) {
+			return super.findCloseOffset(line, lineOffset);
+		}
+		return nestedBlock.findCloseOffset(line, lineOffset);
+	}
+
+	@Override
+	public boolean beginNesting() {
+		if (nestedBlock == null) {
+			return super.beginNesting();
+		}
+		return nestedBlock.beginNesting();
+	}
+
+	@Override
+	public boolean canResume(String line, int lineOffset) {
+		if (nestedBlock == null) {
+			return super.canResume(line, lineOffset);
+		}
+		return nestedBlock.canResume(line, lineOffset);
 	}
 
 	@Override
diff --git a/wikitext/core/org.eclipse.mylyn.wikitext.confluence/src/test/java/org/eclipse/mylyn/wikitext/confluence/ConfluenceLanguageTest.java b/wikitext/core/org.eclipse.mylyn.wikitext.confluence/src/test/java/org/eclipse/mylyn/wikitext/confluence/ConfluenceLanguageTest.java
index a89b234..ab2897e 100644
--- a/wikitext/core/org.eclipse.mylyn.wikitext.confluence/src/test/java/org/eclipse/mylyn/wikitext/confluence/ConfluenceLanguageTest.java
+++ b/wikitext/core/org.eclipse.mylyn.wikitext.confluence/src/test/java/org/eclipse/mylyn/wikitext/confluence/ConfluenceLanguageTest.java
@@ -150,6 +150,20 @@
 	}
 
 	@Test
+	public void testBlockQuoteExtendedWithNestedTable() {
+		assertMarkup(
+				"<blockquote><table><tr><td>Names</td><td>Occupation</td></tr><tr><td><ul><li>John</li><li>Jane</li></ul></td><td>Programmer</td></tr></table></blockquote>",
+				"{quote}|Names|Occupation|\n|* John\n* Jane|Programmer|{quote}\n");
+	}
+
+	@Test
+	public void testBlockQuoteExtendedWithMultipleNestedBlocks() {
+		assertMarkup(
+				"<blockquote><table><tr><td>Names</td><td>Occupation</td></tr><tr><td><ul><li>John</li><li>Jane</li></ul></td><td>Programmer</td></tr></table><ul><li>another</li><li>list</li></ul><p>and a para</p></blockquote>",
+				"{quote}\n|Names|Occupation|\n|* John\n* Jane|Programmer|\n\n* another\n* list\n\nand a para{quote}\n");
+	}
+
+	@Test
 	public void testSimplePhraseModifiers() throws IOException {
 		Object[][] pairs = new Object[][] { { "*", "strong" }, { "_", "em" }, { "??", "cite" }, { "-", "del" },
 				{ "+", "u" }, { "^", "sup" }, { "~", "sub" }, };
diff --git a/wikitext/core/org.eclipse.mylyn.wikitext.confluence/src/test/java/org/eclipse/mylyn/wikitext/confluence/internal/ConfluenceDocumentBuilderTest.java b/wikitext/core/org.eclipse.mylyn.wikitext.confluence/src/test/java/org/eclipse/mylyn/wikitext/confluence/internal/ConfluenceDocumentBuilderTest.java
index ed3a6e1..8b4f612 100644
--- a/wikitext/core/org.eclipse.mylyn.wikitext.confluence/src/test/java/org/eclipse/mylyn/wikitext/confluence/internal/ConfluenceDocumentBuilderTest.java
+++ b/wikitext/core/org.eclipse.mylyn.wikitext.confluence/src/test/java/org/eclipse/mylyn/wikitext/confluence/internal/ConfluenceDocumentBuilderTest.java
@@ -660,6 +660,28 @@
 	}
 
 	@Test
+	public void nestTablesAfterBlockQuote() {
+		builder.beginDocument();
+		builder.beginBlock(BlockType.QUOTE, new Attributes());
+		builder.beginBlock(BlockType.TABLE, new Attributes());
+		builder.beginBlock(BlockType.TABLE_ROW, new Attributes());
+		builder.beginBlock(BlockType.TABLE_CELL_NORMAL, new Attributes());
+		builder.beginBlock(BlockType.BULLETED_LIST, new Attributes());
+		emitListItemHavingParagraphAndContent("first");
+		emitListItemHavingParagraphAndContent("second");
+		builder.endBlock();
+		builder.endBlock();
+		builder.endBlock();
+		builder.endBlock();
+		builder.endBlock();
+		builder.endDocument();
+
+		String markup = out.toString();
+
+		assertEquals("{quote}|* first\n* second|\n\n{quote}\n\n", markup);
+	}
+
+	@Test
 	public void tableWithNestedList() {
 		assertTableRow("| |* first\n" + //
 				"* second| |\n\n", //
diff --git a/wikitext/core/org.eclipse.mylyn.wikitext.confluence/src/test/java/org/eclipse/mylyn/wikitext/confluence/internal/block/ExtendedQuoteBlockTest.java b/wikitext/core/org.eclipse.mylyn.wikitext.confluence/src/test/java/org/eclipse/mylyn/wikitext/confluence/internal/block/ExtendedQuoteBlockTest.java
new file mode 100644
index 0000000..e392fa3
--- /dev/null
+++ b/wikitext/core/org.eclipse.mylyn.wikitext.confluence/src/test/java/org/eclipse/mylyn/wikitext/confluence/internal/block/ExtendedQuoteBlockTest.java
@@ -0,0 +1,37 @@
+/*******************************************************************************
+ * Copyright (c) 2007, 2008 David Green and others.
+ * All rights reserved. This program and 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:
+ *     David Green - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.mylyn.wikitext.confluence.internal.block;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+public class ExtendedQuoteBlockTest {
+
+	@Test
+	public void beginNestingWithoutANestedBlock() {
+		ExtendedQuoteBlock block = new ExtendedQuoteBlock();
+		assertEquals(false, block.beginNesting());
+	}
+
+	@Test
+	public void canResumeWithoutANestedBlock() {
+		ExtendedQuoteBlock block = new ExtendedQuoteBlock();
+		assertEquals(false, block.canResume("some line", 1));
+	}
+
+	@Test
+	public void finadCloseOffsetWithoutANestBlock() {
+		ExtendedQuoteBlock block = new ExtendedQuoteBlock();
+		assertEquals(-1, block.findCloseOffset("some line", 1));
+	}
+}