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

org.duelengine.duel.parsing.DuelParser Maven / Gradle / Ivy

package org.duelengine.duel.parsing;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

import org.duelengine.duel.ast.CALLCommandNode;
import org.duelengine.duel.ast.CodeCommentNode;
import org.duelengine.duel.ast.CommandNode;
import org.duelengine.duel.ast.CommentNode;
import org.duelengine.duel.ast.ContainerNode;
import org.duelengine.duel.ast.DocTypeNode;
import org.duelengine.duel.ast.DuelNode;
import org.duelengine.duel.ast.ElementNode;
import org.duelengine.duel.ast.ExpressionNode;
import org.duelengine.duel.ast.FORCommandNode;
import org.duelengine.duel.ast.IFCommandNode;
import org.duelengine.duel.ast.LiteralNode;
import org.duelengine.duel.ast.MarkupExpressionNode;
import org.duelengine.duel.ast.PARTCommandNode;
import org.duelengine.duel.ast.StatementNode;
import org.duelengine.duel.ast.UnknownNode;
import org.duelengine.duel.ast.VIEWCommandNode;
import org.duelengine.duel.ast.XORCommandNode;

/**
 * Processes a token sequence into AST
 */
public class DuelParser {

	private DuelToken next;
	private Iterator tokens;

	/**
	 * Parses token sequence into AST
	 * @param tokenSequence
	 * @return
	 */
	public List parse(DuelToken... tokenSequence)
		throws Exception {

		return parse(tokenSequence != null ? Arrays.asList(tokenSequence).iterator() : null);
	}

	/**
	 * Parses token sequence into AST
	 * @param tokenSequence
	 * @return
	 */
	public List parse(Iterable tokenSequence)
		throws Exception {

		return parse(tokenSequence != null ? tokenSequence.iterator() : null);
	}

	/**
	 * Parses token sequence into AST
	 * @param tokenSequence
	 * @return
	 */
	public List parse(Iterator tokenSequence)
		throws IOException {

		if (tokenSequence == null) {
			throw new NullPointerException("tokenSequence");
		}

		tokens = tokenSequence;
		try {

			ContainerNode document = new ContainerNode(0, 0, 0);
			while (hasNext()) {
				parseNext(document);
			}

			List views = new ArrayList(1);
			for (DuelNode node : document.getChildren()) {
				if (node instanceof VIEWCommandNode) {
					views.add(scrubView((VIEWCommandNode)node));
					continue;
				}

				if (node instanceof LiteralNode) {
					String text = ((LiteralNode)node).getValue();
					if (text == null || text.trim().isEmpty()) {
						continue;
					}
				}

				// syntax error unless is literal whitespace
				throw new InvalidNodeException("Content must sit within a named view.", node);
			}

			return views;
			
		} finally {
			tokens = null;
			next = null;
		}
	}

	/**
	 * Processes the next node
	 * @param parent
	 * @throws Exception
	 */
	private void parseNext(ContainerNode parent)
		throws IOException {
		switch (next.getToken()) {
			case LITERAL:
				parseLiteral(parent);
				break;

			case ELEM_BEGIN:
				parseElem(parent);
				break;

			case ELEM_END:
				if (parent instanceof ElementNode) {
					ElementNode parentElem = (ElementNode)parent;
					if (parentElem.isAncestorOrSelf(next.getValue())) {
						// pass on up
						return;
					}
				}

				// ignore extraneous close tags
				// consume token
				next = null;
				break;

			case BLOCK:
				parseBlock(parent);
				break;

			case ERROR:
				// TODO: back with interface?
				if (tokens instanceof DuelLexer) {
					throw new InvalidTokenException("Syntax error: "+next.getValue(), next, ((DuelLexer)tokens).getLastError());
				}

				throw new InvalidTokenException("Syntax error: "+next.getValue(), next);

			default:
				throw new InvalidTokenException("Invalid token: "+next, next);
		}
	}

	/**
	 * Parses the next token into a literal text node
	 * @param parent
	 */
	private void parseLiteral(ContainerNode parent) {
		DuelNode last = parent.getLastChild();

		if (last instanceof LiteralNode) {
			LiteralNode lastLit = ((LiteralNode)last);

			// perform constant folding of literal strings
			lastLit.setValue(lastLit.getValue() + next.getValue());
		} else {
			// add directly to output
			parent.appendChild(new LiteralNode(next.getValue(), next.getIndex(), next.getLine(), next.getColumn()));
		}

		// consume token
		next = null;
	}

	/**
	 * Parses the next sequence of tokens into a new element node
	 * @param parent
	 */
	private void parseElem(ContainerNode parent)
		throws IOException {

		String tagName = next.getValue();
		ElementNode elem = createElement(tagName, next.getIndex(), next.getLine(), next.getColumn());
		parent.appendChild(elem);

		// consume token
		next = null;
		String attrName = null;

		while (hasNext()) {
			switch (next.getToken()) {
				case ATTR_NAME:
					attrName = next.getValue();
					// set just in case no value
					elem.setAttribute(attrName, null);

					// consume token
					next = null;
					break;

				case ATTR_VALUE:
					if (attrName == null) {
						throw new InvalidTokenException("Attribute name was missing", next);
					}

					BlockValue block = next.getBlock();
					DuelNode attrVal;
					if (block != null) {
						attrVal = createBlock(block, next.getIndex(), next.getLine(), next.getColumn());
					} else {
						attrVal = new LiteralNode(next.getValue(), next.getIndex(), next.getLine(), next.getColumn());
					}
					elem.setAttribute(attrName, attrVal);
					attrName = null;

					// consume token
					next = null;
					break;

				case ELEM_END:
					String tag = next.getValue();
					if (tag != null) {
						tag = tag.toLowerCase();
						if (elem.isSelf(tag)) {
							// consume token
							next = null;

							rewriteConditionalAttr(elem);
							return;
						}
						if (elem.isAncestor(tag)) {
							rewriteConditionalAttr(elem);

							// pass next on up
							return;
						}
					}

					// ignore extraneous close tags
					// consume token
					next = null;
					break;

				default:
					if (!elem.canHaveChildren()) {
						rewriteConditionalAttr(elem);

						// pass next on up
						return;
					}
					parseNext(elem);
					break;
			}
		}
	}

	/**
	 * Parses the next token into a block node
	 * @param parent
	 */
	private void parseBlock(ContainerNode parent) {
		BlockValue block = next.getBlock();

		if (block != null) {
			DuelNode node = createBlock(block, next.getIndex(), next.getLine(), next.getColumn());
			if (node != null) {
				parent.appendChild(node);
			}
		}

		// consume token
		next = null;
	}

	private void rewriteConditionalAttr(ElementNode elem) {
		if (elem instanceof CommandNode && !(elem instanceof CALLCommandNode) && !(elem instanceof FORCommandNode)) {
			// only process normal CALL, FOR, and HTML elements
			return;
		}

		DuelNode attr = elem.removeAttribute(IFCommandNode.IF_ATTR);
		if (attr == null) {
			// nothing to do
			return;
		}

		// create a conditional wrapper and
		// move attr over to the conditional
		IFCommandNode conditional = new IFCommandNode(attr.getIndex(), attr.getLine(), attr.getColumn());
		conditional.setAttribute(IFCommandNode.TEST, attr);

		// use the actual parent node rather than the source parent
		ContainerNode parent = elem.getParent();

		// wrap element in parent
		if (!parent.replaceChild(conditional, elem)) {
			throw new IllegalStateException("Conditional rewrite failed inside "+parent.getClass().getSimpleName());
		}
		conditional.appendChild(elem);
	}

	private VIEWCommandNode scrubView(VIEWCommandNode node) {
		if (node.getName() == null || node.getName().isEmpty()) {
			// syntax error
			throw new InvalidNodeException("View is missing name attribute", node);
		}

		// remove leading and trailing pure whitespace nodes
		
		DuelNode child = node.getLastChild();
		if (child instanceof LiteralNode) {
			String text = ((LiteralNode)child).getValue();
			if (CharUtility.isNullOrWhiteSpace(text)) {
				node.removeChild(child);
			}
		}

		child = node.getFirstChild();
		if (child instanceof LiteralNode) {
			String text = ((LiteralNode)child).getValue();
			if (CharUtility.isNullOrWhiteSpace(text)) {
				node.removeChild(child);
			}
		}
		
		return node;
	}

	/**
	 * Ensures the next node is ready
	 * @return
	 */
	private boolean hasNext() {
		// ensure non-null value
		while (next == null && tokens.hasNext()) {
			next = tokens.next();
		}

		return (next != null);
	}

	/**
	 * ElementNode factory method
	 * @param tagName
	 * @param index
	 * @param line
	 * @param column
	 * @return
	 */
	public static ElementNode createElement(String tagName, int index, int line, int column) {

		if (tagName == null) {
			return null;
		}

		if (tagName.equalsIgnoreCase(FORCommandNode.EXT_NAME)) {
			return new FORCommandNode(index, line, column);
		}

		if (tagName.equalsIgnoreCase(XORCommandNode.EXT_NAME)) {
			return new XORCommandNode(index, line, column);
		}

		if (tagName.equalsIgnoreCase(IFCommandNode.EXT_NAME)) {
			return new IFCommandNode(index, line, column);
		}

		if (tagName.equalsIgnoreCase(CALLCommandNode.EXT_NAME)) {
			return new CALLCommandNode(index, line, column);
		}

		if (tagName.equalsIgnoreCase(PARTCommandNode.EXT_NAME)) {
			return new PARTCommandNode(index, line, column);
		}

		if (tagName.equalsIgnoreCase(VIEWCommandNode.EXT_NAME)) {
			return new VIEWCommandNode(index, line, column);
		}

		return new ElementNode(tagName.toLowerCase(), index, line, column);
	}

	/**
	 * BlockNode factory method
	 * @param block
	 * @return
	 */
	private DuelNode createBlock(BlockValue block, int index, int line, int column) {

		String begin = block.getBegin();
		if (begin == null) {
			return null;
		}

		String value = block.getValue();

		if (begin.equals(ExpressionNode.BEGIN)) {
			return new ExpressionNode(value, index, line, column);
		}

		if (begin.equals(StatementNode.BEGIN)) {
			return new StatementNode(value, index, line, column);
		}

		if (begin.equals(MarkupExpressionNode.BEGIN)) {
			return new MarkupExpressionNode(value, index, line, column);
		}

		if (begin.equalsIgnoreCase(DocTypeNode.BEGIN)) {
			return new DocTypeNode(value, index, line, column);
		}

		if (begin.equalsIgnoreCase(CommentNode.BEGIN)) {
			return new CommentNode(value, index, line, column);
		}

		if (begin.equalsIgnoreCase(CodeCommentNode.BEGIN)) {
			return new CodeCommentNode(value, index, line, column);
		}

		// others are emitted directly
		return new UnknownNode(begin+value+block.getEnd(), column, column, column);
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy