/*******************************************************************************
* Copyright (c) 2015 Bruno Medeiros and other Contributors.
* 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:
* Bruno Medeiros - initial API and implementation
*******************************************************************************/
package melnorme.lang.tooling.parser;
import static melnorme.utilbox.core.Assert.AssertNamespace.assertNotNull;
import static melnorme.utilbox.core.Assert.AssertNamespace.assertTrue;
import static melnorme.utilbox.core.CoreUtil.areEqual;
import static melnorme.utilbox.core.CoreUtil.arrayC;
import java.text.MessageFormat;
import melnorme.lang.utils.parse.BasicCharSource_CommonExceptionAdapter;
import melnorme.lang.utils.parse.IBasicCharSource;
import melnorme.lang.utils.parse.LexingUtils;
import melnorme.utilbox.core.CommonException;
import melnorme.utilbox.misc.ArrayUtil;
public class TextBlocksReader {
public enum TokenKind {
TEXT,
BLOCK_OPEN,
EOS,
}
/* ----------------- ----------------- */
protected final BasicCharSource_CommonExceptionAdapter charSource;
protected final char[] blockOpens;
protected final char[] blockCloses;
protected String lastValue;
protected TokenKind lastKind;
public TextBlocksReader(IBasicCharSource<? extends Exception> charSource) {
this(charSource,
arrayC('{', '(', '['),
arrayC('}', ')', ']')
);
}
public TextBlocksReader(IBasicCharSource<? extends Exception> charSource,
char[] blockOpens, char[] blockCloses) {
this.charSource = new BasicCharSource_CommonExceptionAdapter(assertNotNull(charSource));
this.blockOpens = assertNotNull(blockOpens);
this.blockCloses = assertNotNull(blockCloses);
assertTrue(blockOpens.length == blockCloses.length);
}
public int peekTokenStart() throws CommonException {
LexingUtils.skipWhitespace(charSource);
return charSource.lookahead();
}
protected char consumeChar() throws CommonException {
lastKind = null;
lastValue = null;
return charSource.consume();
}
/* ----------------- ----------------- */
public String setTokenResult(String value, TokenKind kind) {
this.lastValue = value;
this.lastKind = kind;
return value;
}
public TokenKind tokenAhead() throws CommonException {
int tokenStartChar = peekTokenStart();
if(tokenStartChar == -1) {
return TokenKind.EOS;
}
char ch = (char) tokenStartChar;
if(charIsBlockOpen(ch)) {
return TokenKind.BLOCK_OPEN;
}
if(charIsBlockClose(ch)) {
return TokenKind.EOS;
}
return TokenKind.TEXT;
}
protected boolean charIsBlockOpen(char ch) {
return ArrayUtil.indexOf(blockOpens, ch) != -1;
}
protected boolean charIsBlockClose(char ch) {
return ArrayUtil.indexOf(blockCloses, ch) != -1;
}
public boolean aheadIsEnd() throws CommonException {
return tokenAhead() == TokenKind.EOS;
}
public boolean aheadIsBlockStart() throws CommonException {
return tokenAhead() == TokenKind.BLOCK_OPEN;
}
public boolean aheadIsText() throws CommonException {
return tokenAhead() == TokenKind.TEXT;
}
public String consumeText() throws CommonException {
if(tokenAhead() != TokenKind.TEXT) {
throw createParseException("Expected text, {0}.", errorAtTokenStart());
}
return parseTextValue();
}
protected String parseTextValue() throws CommonException {
peekTokenStart();
StringBuilder sb = new StringBuilder();
while(charSource.hasCharAhead()) {
char ahead = charSource.lookaheadChar();
if(Character.isWhitespace(ahead) || charIsBlockClose(ahead) || charIsBlockOpen(ahead)) {
break;
}
char ch = consumeChar();
if(ch == '"') {
boolean isEOS = LexingUtils.consumeUntilDelimiter_intoStringBuilder(charSource, '"', '\\', sb);
if(isEOS) {
throw createParseException("Unterminated text `{0}`.", sb.toString());
}
} else {
sb.append(ch);
}
}
return setTokenResult(sb.toString(), TokenKind.TEXT);
}
/* ----------------- ----------------- */
public TextBlocksSubReader enterBlock() throws CommonException {
if(tokenAhead() != TokenKind.BLOCK_OPEN) {
throw createParseException("Expected block open, {0}.", errorAtTokenStart());
}
return parseBlock();
}
protected TextBlocksSubReader parseBlock() throws CommonException {
char ch = consumeChar();
int ix = ArrayUtil.indexOf(blockOpens, ch);
char expectedClose = blockCloses[ix];
return new TextBlocksSubReader(this.charSource, this.blockOpens, this.blockCloses, expectedClose);
}
public static class TextBlocksSubReader extends TextBlocksReader implements AutoCloseable {
protected final char expectedClose;
public TextBlocksSubReader(IBasicCharSource<? extends Exception> charSource, char[] blockOpens,
char[] blockCloses, char expectedClose) {
super(charSource, blockOpens, blockCloses);
this.expectedClose = expectedClose;
}
@Override
public void close() throws CommonException {
int ahead = peekTokenStart();
if(ahead == expectedClose) {
char ch = charSource.consume();
assertTrue(ch == ahead);
} else {
throw createParseException("Expected BLOCK_CLOSE `{0}`, {1}.", expectedClose, errorAtTokenStart());
}
}
}
/* ----------------- Error handling ----------------- */
protected CommonException createParseException(String pattern, Object... arguments) {
return new CommonException(MessageFormat.format(pattern, arguments));
}
public String errorAtTokenStart() throws CommonException {
return found(peekTokenStart());
}
protected String found(int ahead) {
if(ahead == -1) {
return "found EOS";
}
char ch = (char) ahead;
return "found `" + ch + "`";
}
/* ----------------- Helpers ----------------- */
public void expectText(String expectedText) throws CommonException {
if(tokenAhead() != TokenKind.TEXT) {
throw createParseException("Expected text `{0}`, {1}.", expectedText, errorAtTokenStart());
}
String text = parseTextValue();
if(!areEqual(text, expectedText)) {
throw createParseException("Expected text `{0}`, found text `{1}`.", expectedText, text);
}
}
public <RET, EXC extends Exception> RET consumeBlock(BlockVisitorX<RET, EXC> visitor)
throws CommonException, EXC {
try(TextBlocksSubReader subReader = enterBlock()) {
return visitor.consumeChildren(subReader);
}
}
public interface BlockVisitorX<RET, EXC extends Exception> {
public RET consumeChildren(TextBlocksSubReader subReader) throws EXC;
}
public void skipNextElement() throws CommonException {
if(aheadIsEnd()) {
throw createParseException("Expected `{0}`, {1}.", "element", errorAtTokenStart());
} else if(aheadIsText()) {
consumeText();
} else {
consumeBlock((subReader) -> {
subReader.skipToEnd();
return null;
});
}
}
public void skipToEnd() throws CommonException {
while(!aheadIsEnd()) {
skipNextElement();
}
}
}