blob: 3514320781390dcbce2c2ce99e487ef26654ca74 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2011, 2015 Tasktop Technologies
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* David Green - initial API and implementation
*******************************************************************************/
package org.eclipse.mylyn.wikitext.parser.builder;
import java.io.IOException;
import java.io.Writer;
import java.util.Stack;
import org.eclipse.mylyn.wikitext.parser.Attributes;
import org.eclipse.mylyn.wikitext.parser.DocumentBuilder;
import com.google.common.base.Throwables;
/**
* Provides default functionality for document builders that emit lightweight wiki markup.
*
* @author David Green
* @since 3.0
*/
public abstract class AbstractMarkupDocumentBuilder extends DocumentBuilder {
/**
* Represents a block or section of the document. By default blocks have no content.
*/
protected abstract class Block {
private Block previousBlock;
private final BlockType blockType;
protected Block(BlockType blockType) {
this.blockType = blockType;
}
public void write(int c) throws IOException {
}
public void write(String s) throws IOException {
}
public void open() throws IOException {
previousBlock = currentBlock;
currentBlock = this;
}
public void close() throws IOException {
currentBlock = previousBlock;
}
public Block getPreviousBlock() {
return previousBlock;
}
public BlockType getBlockType() {
return blockType;
}
/**
*
*/
protected boolean isImplicitBlock() {
return false;
}
}
/**
* a block that outputs a suffix when it's closed
*/
protected class SuffixBlock extends Block {
private final String suffix;
public SuffixBlock(BlockType blockType, String suffix) {
super(blockType);
this.suffix = suffix;
}
@Override
public void close() throws IOException {
emitContent(suffix);
super.close();
}
}
/**
* A block that is delimited by newlines.
*/
protected class NewlineDelimitedBlock extends Block {
private final int precedingNewlineCount;
private final int trailingNewlineCount;
public NewlineDelimitedBlock(BlockType blockType, int precedingNewlineCount, int trailingNewlineCount) {
super(blockType);
this.precedingNewlineCount = precedingNewlineCount;
this.trailingNewlineCount = trailingNewlineCount;
}
@Override
public void open() throws IOException {
super.open();
emitDelimiter(precedingNewlineCount);
}
private void emitDelimiter(int newlineCount) throws IOException {
if (getLastChar() != 0) {
int delimiterSize = newlineCount - getTrailingNewlineCount();
for (int x = delimiterSize; x > 0; --x) {
emitContent('\n');
}
}
}
@Override
public void close() throws IOException {
emitDelimiter(trailingNewlineCount);
super.close();
}
}
/**
* a block that provides default paragraph functionality, for emitting content when no explicit block has been
* opened.
*/
protected class ImplicitParagraphBlock extends Block {
private boolean hasContent = false;
public ImplicitParagraphBlock() {
super(BlockType.PARAGRAPH);
}
@Override
public void write(int c) throws IOException {
hasContent = true;
emitContent(normalizeWhitespace(c));
}
@Override
public void write(String s) throws IOException {
hasContent = true;
emitContent(normalizeWhitespace(s));
}
@Override
public void close() throws IOException {
if (hasContent) {
emitContent("\n\n"); //$NON-NLS-1$
}
super.close();
}
/**
*
*/
protected int normalizeWhitespace(int c) {
return AbstractMarkupDocumentBuilder.this.normalizeWhitespace(c);
}
/**
*
*/
protected String normalizeWhitespace(String s) {
return AbstractMarkupDocumentBuilder.this.normalizeWhitespace(s);
}
@Override
protected boolean isImplicitBlock() {
return true;
}
}
protected Block currentBlock;
private Stack<MarkupWriter> writerState;
private MarkupWriter writer;
private boolean adjacentSeparatorRequired = false;
private static class MarkupWriter extends Writer {
private final Writer delegate;
private char lastChar;
private int trailingNewlineCount;
private int characterCount;
public MarkupWriter(Writer delegate) {
this.delegate = delegate;
}
@Override
public void write(char[] cbuf, int off, int len) throws IOException {
if (len <= 0) {
return;
}
delegate.write(cbuf, off, len);
characterCount += len;
int lastCharIndex = off + len - 1;
lastChar = cbuf[lastCharIndex];
int newlineCount = countTailingNewlines(cbuf, off, len);
if (newlineCount == len) {
trailingNewlineCount += newlineCount;
} else {
trailingNewlineCount = newlineCount;
}
}
private int countTailingNewlines(char[] cbuf, int off, int len) {
int newlineCount = 0;
for (int x = off + len - 1; x >= off; --x) {
char c = cbuf[x];
if (c != '\n') {
break;
}
++newlineCount;
}
return newlineCount;
}
/**
* get the last character that was written to the writer, or 0 if no character has been written.
*/
public char getLastChar() {
return lastChar;
}
public int getTrailingNewlineCount() {
return trailingNewlineCount;
}
public int getCharacterCount() {
return characterCount;
}
@Override
public void flush() throws IOException {
delegate.flush();
}
@Override
public void close() throws IOException {
delegate.close();
}
public Writer getDelegate() {
return delegate;
}
}
protected AbstractMarkupDocumentBuilder(final Writer out) {
this.writer = new MarkupWriter(out);
}
protected void emitContent(int c) throws IOException {
maybeInsertAdjacentWhitespace(c);
writer.write(c);
}
private void maybeInsertAdjacentWhitespace(int c) throws IOException {
if (adjacentSeparatorRequired) {
if (!isSeparator(c)) {
char lastChar = getLastChar();
if (lastChar != 0 && !isSeparator(lastChar)) {
writer.write(' ');
}
}
adjacentSeparatorRequired = false;
}
}
/**
* Emits the given string
*
* @param str
* the string to emit, or null
* @throws IOException
* if the writer throws an IOException
*/
protected void emitContent(String str) throws IOException {
if (str == null || str.length() == 0) {
return;
}
maybeInsertAdjacentWhitespace(str.charAt(0));
writer.write(str);
}
/**
* Indicate that the next content to be emitted requires adjacent {@link #isSeparator(char) separator}. When
* invoked, the next call to {@link #emitContent(int)} or {@link #emitContent(String)} will test to see if the
* {@link #getLastChar() last character} is a separator character, or if the content to be emitted starts with a
* separator. If neither are true, then a single space character is inserted into the content stream. Subsequent
* calls to <code>emitContent</code> are not affected.
*
* @see #clearRequireAdjacentSeparator()
*/
protected void requireAdjacentSeparator() {
adjacentSeparatorRequired = true;
}
/**
* @see #requireAdjacentSeparator()
*/
protected void clearRequireAdjacentSeparator() {
adjacentSeparatorRequired = false;
}
protected boolean isSeparator(int i) {
char c = (char) i;
boolean separator = Character.isWhitespace(c);
if (!separator) {
switch (c) {
case ',':
case '.':
case '!':
case '?':
case ':':
case ';':
case ')':
case '(':
case '}':
case '{':
case '[':
case ']':
case '|':
case '"':
separator = true;
}
}
return separator;
}
@Override
public void beginDocument() {
// nothing to do
}
@Override
public void endDocument() {
flush();
}
@Override
public void flush() {
while (currentBlock != null) {
endBlock();
}
}
/**
* Subclasses may push a writer in order to intercept emitted content. Calls to this method must be matched by
* corresponding calls to {@link #popWriter()}.
*
* @see #popWriter()
*/
protected void pushWriter(Writer writer) {
if (writerState == null) {
writerState = new Stack<MarkupWriter>();
}
writerState.push(this.writer);
this.writer = new MarkupWriter(writer);
}
/**
* @see #pushWriter(Writer)
*/
protected Writer popWriter() {
if (writerState == null || writerState.isEmpty()) {
throw new IllegalStateException();
}
MarkupWriter markupWriter = writer;
writer = writerState.pop();
return markupWriter.getDelegate();
}
/**
* get the last character that was emitted, or 0 if no character has been written.
*/
protected char getLastChar() {
char c = writer.getLastChar();
if (c == 0 && writerState != null) {
for (int x = writerState.size() - 1; c == 0 && x >= 0; --x) {
c = writerState.get(x).getLastChar();
}
}
return c;
}
/**
* Provides the number of trailing newlines that were emitted, or 0 if no trailing newlines were written.
*/
protected int getTrailingNewlineCount() {
int count = writer.getTrailingNewlineCount();
if (writer.getCharacterCount() == count) {
for (int x = writerState.size() - 1; x >= 0; --x) {
MarkupWriter markupWriter = writerState.get(x);
int trailingNewlineCount = markupWriter.getTrailingNewlineCount();
count += trailingNewlineCount;
if (markupWriter.getCharacterCount() > trailingNewlineCount) {
break;
}
}
}
return count;
}
@Override
public void beginBlock(BlockType type, Attributes attributes) {
try {
if (currentBlock != null && currentBlock.isImplicitBlock()) {
currentBlock.close();
currentBlock = null;
}
Block block = computeBlock(type, attributes);
block.open();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
protected abstract Block computeBlock(BlockType type, Attributes attributes);
@Override
public void beginSpan(SpanType type, Attributes attributes) {
assertOpenBlock();
Block block = computeSpan(type, attributes);
try {
block.open();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
protected abstract Block computeSpan(SpanType type, Attributes attributes);
@Override
public void endSpan() {
closeCurrentBlock();
}
protected String computePrefix(char c, int count) {
String prefix = ""; //$NON-NLS-1$
while (count-- > 0) {
prefix += c;
}
return prefix;
}
protected int computeListLevel() {
int level = 0;
Block b = currentBlock;
while (b != null) {
if (b.getBlockType() != null) {
switch (b.getBlockType()) {
case BULLETED_LIST:
case NUMERIC_LIST:
case DEFINITION_LIST:
++level;
}
}
b = b.getPreviousBlock();
}
return level;
}
protected BlockType computeCurrentListType() {
Block b = currentBlock;
while (b != null) {
if (b.getBlockType() != null) {
switch (b.getBlockType()) {
case BULLETED_LIST:
case NUMERIC_LIST:
case DEFINITION_LIST:
return b.getBlockType();
}
}
b = b.getPreviousBlock();
}
return null;
}
@Override
public void endBlock() {
closeCurrentBlock();
}
private void closeCurrentBlock() {
if (currentBlock != null) {
try {
currentBlock.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
@Override
public void beginHeading(int level, Attributes attributes) {
Block block = computeHeading(level, attributes);
try {
block.open();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void endHeading() {
endBlock();
}
protected abstract Block computeHeading(int level, Attributes attributes);
protected void assertOpenBlock() {
if (currentBlock == null) {
Block block = createImplicitParagraphBlock();
try {
block.open();
} catch (IOException e) {
throw Throwables.propagate(e);
}
}
}
@Override
public void charactersUnescaped(String literal) {
characters(literal);
}
protected int normalizeWhitespace(int c) {
if (c == '\r' || c == '\n') {
c = ' ';
}
return c;
}
protected String normalizeWhitespace(String s) {
s = s.replaceAll("(\r|\n)", " "); //$NON-NLS-1$//$NON-NLS-2$
return s;
}
/**
* Creates paragraph blocks in cases where content is emitted but no block is currently open. Subclasses may
* override to alter the default paragraph block implementation.
*/
protected Block createImplicitParagraphBlock() {
return new ImplicitParagraphBlock();
}
}