package tikzparser;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import main.Debug;
import tikzmodel.KeyValue;
import tikzmodel.TeXDimension;
import tikzmodel.TeXUnit;
import tikzmodel.TikzCircle;
import tikzmodel.TikzCoordinate;
import tikzmodel.TikzFigure;
import tikzmodel.TikzLine;
import tikzmodel.TikzOperator;
import tikzmodel.TikzParameters;
import tikzmodel.TikzPath;
import tikzmodel.TikzPathType;
import tikzmodel.TikzPicture;
import tikzmodel.TikzRectangle;
/**
* This is a TikZ-parser that creates various syntactical data structures from
* a given TikZ source code.
*
* @author Florian Noack
*
*/
public class TikzParser implements Debug {
// Floating-point fields
public static final String FP_REGEX;
public static final Pattern FP_PATTERN;
// TikZ-path separator
public static final Pattern PATH_SEPARATOR_PATTERN;
static {
// Floating-point fields
FP_REGEX = "[+-]?" + // Optional sign character
"[\\d]*" + // Optional before floating-point
"[.]?" + // Optional floating-point
"\\d+"; // Digits
FP_PATTERN = Pattern.compile(FP_REGEX);
// TikZ-path separator pattern
PATH_SEPARATOR_PATTERN
= Pattern.compile(new Character(TikzPath.PATH_SEPARATOR).toString());
}
/**
* Offset during the parsing routine
*/
private int offset;
/**
* Code to be parsed
*/
private char[] code;
/*
* Path chain variables
*/
String operatorToken = null;
TikzOperator lastOperator = null;
SyntaxTikzCoordinate origin = null;
SyntaxTikzCoordinate lastOrigin = null;
/**
* Returns a SyntaxTikzPicture from a specified TikZ source code.
*
* @param code
* TikZ source code to be parsed.
*
* @return
* parsed picture.
*/
public SyntaxTikzPicture parseCode(String code) {
offset = -1;
this.code = code.toCharArray();
p("parseCode: #"+code+"#");
TikzPicture tp = new TikzPicture();
SyntaxTikzPicture stxPicture = new SyntaxTikzPicture(tp);
/*
* Read while new path triggers are detected
*/
boolean running = true;
int firstSave = 0;
while(running) {
offset = code.indexOf(TikzPath.PATH_TRIGGER, ++offset);
// No more path triggers found
if(offset == -1) {
stxPicture.addToken(Util.substring(this.code, firstSave, this.code.length-1));
running = false;
} else {
// Try to decode a path and save it
try {
int pathStart = offset;
SyntaxTikzPath path = parsePath();
p("parseCode: new path added");
stxPicture.addToken(Util.substring(this.code, firstSave, pathStart-1));
stxPicture.addToken(path);
firstSave = offset;
--offset;
} catch (TikzParserException e) {
p(e.getMessage());
//e.printStackTrace();
}
}
}
return stxPicture;
}
/**
*
* @return
* @throws TikzParserException
*/
private SyntaxTikzPath parsePath() throws TikzParserException {
p("parsePath()");
/*
* Locals
*/
TikzPath path = null;
ArrayList<TikzFigure> figures = new ArrayList<TikzFigure>();
ArrayList<Object> tokens = new ArrayList<Object>();
/*
* Check path trigger \
*/
if(skipChar() != TikzPath.PATH_TRIGGER) {
throw new TikzParserException(TikzPath.PATH_TRIGGER +" expected");
}
/*
* Read path type (i.e. draw)
*/
TikzPathType pt = readPathType();
tokens.add(pt);
p("PathType gelesen");
tokens.add(readWhitespaces());
p("whitespaces nach PathType gelesen");
/*
* Read parameters (i.e. [color=blue,fill=white])
*/
TikzParameters pathParams = null;
try {
pathParams = parseParameters();
tokens.add(pathParams);
} catch(TikzParserException tpe) {}
p("TikzParameters gelesen");
tokens.add(readWhitespaces());
/*
* Read first coordinate
*/
origin = parseCoordinate();
tokens.add(origin);
/*
* Read path chain (i.e. rectangle (2,4) -- (1,2))
*/
parsePathChain(tokens,figures);
/*
* Construct path and return it
*/
path = new TikzPath(pt, pathParams, figures);
SyntaxTikzPath stp = new SyntaxTikzPath(path,tokens);
return stp;
}
/**
*
*/
private void parsePathChain(ArrayList<Object> tokens,
ArrayList<TikzFigure> figures) throws TikzParserException {
p("readPathChain()");
boolean running = true;
while(running) {
tokens.add(readWhitespaces());
/*
* Check whether this path has ended and stop...
*/
if(readChar() == TikzPath.PATH_SEPARATOR) {
running = false;
lastOperator = null;
offset++;
/*
* ...or go on reading path commands
*/
} else {
/*
* Read PATH OPERATOR like --, rectangle or circle
*
* QUICK AND DIRTY -- REWRITE THIS!
*/
p("reading operatorToken");
if(skipChar()=='-' && readChar()=='-') {
operatorToken = "--";
offset++;
} else {
offset--;
operatorToken = readWord();
}
/*
* Try to convert read PATH OPERATOR
*/
try {
lastOperator = TikzOperator.getValue(operatorToken.toUpperCase());
tokens.add(lastOperator);
tokens.add(readWhitespaces());
} catch(IllegalArgumentException iae) {
throw new TikzParserException("expected TikzOperator but found: " +operatorToken);
}
/*
* Neues Objekt erzeugen?
*/
if(lastOperator != null) {
lastOrigin = origin;
switch(lastOperator) {
case LINETO:
origin = parseCoordinate();
tokens.add(origin);
TikzLine line = new TikzLine(
lastOrigin.getRepresented(),
origin.getRepresented()
);
p("parsePathChain: added: " +line.toString());
figures.add(line);
break;
case RECTANGLE:
origin = parseCoordinate();
tokens.add(origin);
TikzRectangle rect = new TikzRectangle(
lastOrigin.getRepresented(),
origin.getRepresented()
);
p("parsePathChain: added: " +rect.toString());
figures.add(rect);
break;
case CIRCLE:
SyntaxTeXDimension circleDim = parseDimension();
tokens.add(circleDim);
TikzCircle circle = new TikzCircle(
lastOrigin.getRepresented(),
circleDim.getRepresented()
);
p("parsePathChain: added: " +circle.toString());
figures.add(circle);
lastOperator = null;
// Neue Koordinate?
tokens.add(readWhitespaces());
if(readChar() == TikzCoordinate.PREFIX) {
origin = parseCoordinate();
}
break;
default:
throw new TikzParserException("unknown TikzOperator: " +operatorToken);
}
}
// Skip whitespaces
tokens.add(readWhitespaces());
}
} // while
}
/**
*
* @return
* @throws TikzParserException
*/
private TikzPathType readPathType() throws TikzParserException {
TikzPathType pt = null;
String stringType = null;
try {
stringType = readWord();
p("stringType gelesen");
pt = TikzPathType.valueOf(stringType.toUpperCase());
} catch(IllegalArgumentException iae) {
throw new TikzParserException("unknown TikzPathType detected: #" +stringType + "#");
}
return pt;
}
/**
*
* @return
* @throws TikzParserException
*/
private SyntaxTikzCoordinate parseCoordinate() throws TikzParserException {
int start = offset;
p("reading coordinate");
// Skip TikzCoordinate.PREFIX
if(skipChar() != TikzCoordinate.PREFIX) {
throw new TikzParserException("coordinate must start with " +TikzCoordinate.PREFIX);
}
// Skip whitespaces
readWhitespaces();
// Read x-Dimension
SyntaxTeXDimension sdx = parseDimension();
// Skip whitespaces
readWhitespaces();
// Skip Coordinate.SEPARATOR
if(skipChar() != TikzCoordinate.SEPARATOR) {
throw new TikzParserException(TikzCoordinate.SEPARATOR +" expected");
}
// Skip whitespaces
readWhitespaces();
// Read y-Dimension
SyntaxTeXDimension sdy = parseDimension();
// Skip whitespaces
readWhitespaces();
// Skip Coordinate.SUFFIX
if(skipChar() != TikzCoordinate.SUFFIX) {
throw new TikzParserException(TikzCoordinate.SUFFIX +" expected");
}
/*
* Build object.
*/
TikzCoordinate c = new TikzCoordinate(sdx.getRepresented(), sdy.getRepresented());
SyntaxTikzCoordinate sc = new SyntaxTikzCoordinate(c, Util.substring(code,start,offset-1));
return sc;
}
/**
*
* @return
* @throws TikzParserException
*/
private SyntaxTeXDimension parseDimension() throws TikzParserException {
int start = offset;
boolean bracketed = false;
if(readChar() == TeXDimension.PREFIX) {
bracketed = true;
offset++;
readWhitespaces();
}
p("parseDimension: reading numeral");
// Read numeral
String numeral = readNumeral();
if(numeral.trim().length() == 0) {
throw new TikzParserException("SyntaxDimension doesn't contain a valid numeral");
}
// Skip whitespaces
readWhitespaces();
p("reading unit");
// Read unit
TeXUnit unit = TeXUnit.NONE;
if(Character.isLetter(readChar())) {
String strUnit = readWord();
p(strUnit);
unit = TeXUnit.getValue(strUnit.toUpperCase());
}
if(bracketed) {
readWhitespaces();
if(skipChar() != TeXDimension.SUFFIX) {
throw new TikzParserException("dimension is bracketed, but doesn't end with "
+TeXDimension.SUFFIX);
}
}
// Build object
TeXDimension d = new TeXDimension(Float.parseFloat(numeral), unit);
d.setBracketed(bracketed);
SyntaxTeXDimension sd = new SyntaxTeXDimension(d, Util.substring(code,start,offset-1));
return sd;
}
/**
*
* @return
* @throws TikzParserException
*/
private TikzParameters parseParameters() throws TikzParserException {
//int start = offset;
TikzParameters tp = new TikzParameters();
// Make sure the first character is the parameter prefix
if(readChar() != TikzParameters.PARAMETERS_PREFIX) {
throw new TikzParserException( "parameters must start with "
+ TikzParameters.PARAMETERS_PREFIX);
}
// Go ahead with next char
offset++;
// Then read all key-values-pairs
String params
= readWord(TikzParameters.PARAMETERS_SUFFIX);
String[] keyvals
= params.split(String.valueOf(TikzParameters.PARAMETERS_SEPARATOR));
for(String keyval : keyvals) {
/*
* TODO
* Need SyntaxTikzParameters instead of TikzParameters
* so the SyntaxKeyValue can be stored.
*/
tp.addKeyValue(parseKeyValue(keyval).getRepresented());
}
// Make sure the last character is the parameter suffix
if(skipChar() != TikzParameters.PARAMETERS_SUFFIX) {
throw new TikzParserException( "parameters must end with "
+ TikzParameters.PARAMETERS_SUFFIX);
}
return tp;
}
/**
*
* @param keyval
* @return
* @throws TikzParserException
*/
private SyntaxKeyValue parseKeyValue(String keyval) throws TikzParserException {
String[] tokens = keyval.split(KeyValue.KEY_VALUE_SEPARATOR);
KeyValue<String,String> kv = null;
switch(tokens.length) {
case 1:
kv = new KeyValue<String,String>(tokens[0], null);
break;
case 2:
kv = new KeyValue<String,String>(tokens[0], tokens[1]);
break;
default:
throw new TikzParserException("no valid key-value-string detected");
}
return new SyntaxKeyValue(kv, keyval);
}
/**
* Returns a String of whitespaces until the next non-whitespace-chracter
* ist found.
*
* @return
* String of whitespaces.
*/
private String readWhitespaces() throws TikzParserException {
String ret = "";
int start = offset;
// Parse whitespaces.
while(Character.isWhitespace(skipChar()));
if(offset != start+1) {
ret = Util.substring(code, start, offset-2);
}
offset--;
return ret;
}
/**
* Returns the next consecutive char sequence till the given delimiters.
*
* @return
* Found char sequence.
*/
private String readWord(char delimiter) throws TikzParserException {
int start = offset;
// Parse letters.
boolean walk = true;
do {
char c = skipChar();
walk = (c != delimiter);
} while(walk);
offset--; // Move pointer back on the delimiter
return Util.substring(code, start, offset-1);
}
/**
* Returns the next consecutive char sequence.
*
* @return
* Found char sequence.
*/
private String readWord() throws TikzParserException {
int start = offset;
// Parse letters.
while(Character.isLetter(skipChar()));
offset--;
return Util.substring(code, start, offset-1);
}
/**
* Returns the next found numeral as a String that matches the
* following Regex:
*
* FP_REGEX = "[+-]?" + // Optional sign character
* "[\\d]*" + // Optional before floating-point
* "[.]?" + // Optional floating-point
* "\\d+"; // Digits
*
* @return
* Next found numeral group
*/
private String readNumeral() throws TikzParserException {
String numeral = "0";
Matcher m = FP_PATTERN.matcher(
Util.substring(code, offset, code.length-1)
//new String(code, offset, code.length-offset)
);
if(m.find()) {
numeral = m.group();
} else {
throw new TikzParserException("no numeral found");
}
offset += numeral.length();
return numeral;
}
private char skipChar() throws TikzParserException {
try {
return code[offset++];
} catch(NullPointerException npe) {
throw new TikzParserException("code sequence ended unexpectedly");
}
}
private char readChar() throws TikzParserException {
try {
return code[offset];
} catch(NullPointerException npe) {
throw new TikzParserException("code sequence ended unexpectedly");
}
}
/*
* BELOW: Debug methods
*/
public void debug() {
SyntaxTikzPicture p
= parseCode("\\begin{tikzpicture}[bla=blie,bsa] \\draw[color=blue] (1,2) rectangle (3,4); \\end{tikzpicture}");
ArrayList<Object> tokens = p.getTokens();
ArrayList<Object> tokens2 = ((SyntaxTikzPath) tokens.get(1)).tokens;
for(Object o : tokens2) {
System.out.println(o);
}
System.out.println(p.toString());
}
public final void p(String s) {
if(DEBUG_PARSER) {
try {
System.out.println("TikzParser.p: " +offset+" "+code[offset]+" " +s);
} catch(NullPointerException npe) {
System.out.println("TikzParser.p: " +offset+" " +s);
} catch(ArrayIndexOutOfBoundsException aioobe) {
System.out.println("TikzParser.p: " +offset+" " +s);
}
}
}
/*
public static void main(String[] args) {
TikzParser p = new TikzParser();
p.debug();
}
*/
}