All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.mylyn.wikitext.parser.builder.AbstractMarkupDocumentBuilder Maven / Gradle / Ivy

There is a newer version: 4.0.0
Show newest version
/*******************************************************************************
 * 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 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.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 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 emitContent 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();
		}
		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();
	}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy