package flow.netbeans.markdown.highlighter; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; import java.util.Deque; import java.util.List; /** * The {@link MarkdownTokenListBuilder} is used to build a list of tokens during * a depth-first left-to-right traversal of a PegDown AST. This functionality * was extracted from the {@link MarkdownLexerVisitor} to simplify unit testing. * @author Holger Stenger */ public class MarkdownTokenListBuilder { private final Deque<MarkdownLexerVisitor.MarkdownTreeToken> stack; private final List<MarkdownToken> tokens; /** * Creates a {@link MarkdownTokenListBuilder}. The total length of the tokens * created by the builder will be equal to the given total length. If the first * added token does not start at position {@code 0}, the builder will insert * a synthetic token to fill the gap. Likewise, if the last added token does * not end at position {@code totalLength}, the builder will insert a synthetic * token to fill the gap. * @param totalLength The total length of the created tokens. */ public MarkdownTokenListBuilder(int totalLength) { stack = new ArrayDeque<MarkdownLexerVisitor.MarkdownTreeToken>(); tokens = new ArrayList<MarkdownToken>(); // Push guard tree token. stack.push(new MarkdownLexerVisitor.MarkdownTreeToken(MarkdownTokenId.PLAIN, stack.size(), 0, totalLength)); } /** * Add a token which contains no nested tokens. This method is equivalent to * a call to {@link #beginTreeToken(flow.netbeans.markdown.highlighter.MarkdownTokenId, int, int)} * directly followed by a call to {@link #endTreeToken()}. * @param id The token id. * @param startIndex The start index of the token (inclusive). * @param endIndex The end index of the token (exclusive). */ public void addLeafTreeToken(MarkdownTokenId id, int startIndex, int endIndex) { beginTreeToken(id, startIndex, endIndex); endTreeToken(); } /** * Begins a token which can contain nested tokens. The call to this method has * to be matched with a call to {@link #endTreeToken()}. * @param id The token id. * @param startIndex The start index of the token (inclusive). * @param endIndex The end index of the token (exclusive). */ public void beginTreeToken(final MarkdownTokenId id, final int startIndex, final int endIndex) { if (stack.isEmpty()) { throw new IllegalStateException("No tokens can be added after calling build"); } MarkdownLexerVisitor.MarkdownTreeToken topTreeToken = stack.peek(); final int effectiveStartIndex = Math.max(startIndex, topTreeToken.getStartIndex()); final int effectiveEndIndex = Math.min(endIndex, topTreeToken.getEndIndex()); addToken(topTreeToken.getId(), topTreeToken.getRemainderIndex(), effectiveStartIndex); topTreeToken.setRemainderIndex(effectiveEndIndex); MarkdownLexerVisitor.MarkdownTreeToken treeToken = new MarkdownLexerVisitor.MarkdownTreeToken(id, stack.size(), effectiveStartIndex, effectiveEndIndex); // LOG.log(Level.INFO, "Added tree token to stack: {0}", treeToken.toString()); stack.push(treeToken); } /** * Ends a token which has been begun by a call to * {@link #beginTreeToken(flow.netbeans.markdown.highlighter.MarkdownTokenId, int, int)}. */ public void endTreeToken() { if (stack.size() < 2) { // Protect guard tree token. throw new IllegalStateException("No token on stack"); } endTreeTokenUnchecked(); } private void endTreeTokenUnchecked() { MarkdownLexerVisitor.MarkdownTreeToken treeToken = stack.pop(); // LOG.log(Level.INFO, "Removed tree token from stack: {0}", treeToken.toString()); addToken(treeToken.getId(), treeToken.getRemainderIndex(), treeToken.getEndIndex()); } private void addToken(MarkdownTokenId id, int startIndex, int endIndex) { if (startIndex < endIndex) { MarkdownToken token = new MarkdownToken(id, startIndex, endIndex); // LOG.log(Level.INFO, "Added token to list: {0}", token); tokens.add(token); } } /** * Finishes the token list construction. * @return The list of created tokens. */ public List<MarkdownToken> build() { if (stack.size() > 1) { throw new IllegalStateException("Still tokens on stack"); } if (stack.size() == 1) { // Pop guard tree token. endTreeTokenUnchecked(); } return Collections.unmodifiableList(tokens); } }