processing.mode.java.tweak.SketchParser Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of java-mode Show documentation
Show all versions of java-mode Show documentation
Processing is a programming language, development environment, and online community.
This Java Mode package contains the Java mode for Processing IDE.
/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
/*
Part of the Processing project - http://processing.org
Copyright (c) 2012-15 The Processing Foundation
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2
as published by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package processing.mode.java.tweak;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class SketchParser {
public List> colorBoxes;
public List> allHandles;
int intVarCount;
int floatVarCount;
final String varPrefix = "tweakmode";
String[] codeTabs;
boolean requiresComment;
List colorModes;
List> scientificNotations;
// currently is used to ignore numbers in 'setup' and 'settings' functions
List> ignoreFunctions;
List> commentBlocks;
List curlyScopes;
public SketchParser(String[] codeTabs, boolean requiresComment) {
this.codeTabs = codeTabs;
this.requiresComment = requiresComment;
intVarCount = 0;
floatVarCount = 0;
// get all comment blocks
commentBlocks = new ArrayList<>();
for (String code : codeTabs) {
commentBlocks.add(getCommentBlocks(code));
}
// add 'settings' and 'setup' to ignore list (to ignore all numbers there)
ignoreFunctions = new ArrayList<>();
Range settingsRange = getVoidFunctionRange(codeTabs[0], "settings");
Range setupRange = getVoidFunctionRange(codeTabs[0], "setup");
ignoreFunctions.add(Arrays.asList(settingsRange, setupRange));
//Add empty lists for the other tabs so we do not get an index out of bounds error later
for (int i = 0; i < codeTabs.length-1; i++) {
ignoreFunctions.add(new ArrayList());
}
// build curly scope for every character in the code
curlyScopes = new ArrayList<>();
for (String code : codeTabs) {
curlyScopes.add(getCurlyScopes(code));
}
// get all scientific notation (to ignore them)
scientificNotations = getAllScientificNotations();
// find, add, and sort all tweak-able numbers in the sketch
addAllNumbers();
// handle colors
colorModes = findAllColorModes();
//colorBoxes = new ArrayList[codeTabs.length];
createColorBoxes();
createColorBoxesForLights();
// If there is more than one color mode per context, allow only hex and
// webcolors in this context. Currently there is no notion of order of
// execution so we cannot know which color mode relate to a color.
handleMultipleColorModes();
}
private void addAllNumbers() {
allHandles = new ArrayList<>();
addAllDecimalNumbers();
addAllHexNumbers();
addAllWebColorNumbers();
for (List handle : allHandles) {
//Collections.sort(allHandles[i], new HandleComparator());
Collections.sort(handle, new HandleComparator());
}
}
/**
* Get a list of all the numbers in this sketch
* @return
* list of all numbers in the sketch (excluding hexadecimals)
*/
private void addAllDecimalNumbers() {
// for every number found:
// save its type (int/float), name, value and position in code.
Pattern p = Pattern.compile("[\\[\\{<>(),\\t\\s\\+\\-\\/\\*^%!|&=?:~]\\d+\\.?\\d*");
for (int i = 0; i < codeTabs.length; i++) {
List handles = new ArrayList();
allHandles.add(handles);
String c = codeTabs[i];
Matcher m = p.matcher(c);
while (m.find()) {
boolean forceFloat = false;
int start = m.start()+1;
int end = m.end();
if (isInRangeList(start, commentBlocks.get(i))) {
// ignore comments
continue;
}
if (isInRangeList(start, ignoreFunctions.get(i))) {
// ignore numbers in predefined functions
continue;
}
if (requiresComment) {
// only add numbers that have the "// tweak" comment in their line
if (!lineHasTweakComment(start, c)) {
continue;
}
}
// ignore scientific notation (e.g. 1e-6)
boolean found = false;
for (Range r : scientificNotations.get(i)) {
if (r.contains(start)) {
found=true;
break;
}
}
if (found) {
continue;
}
// remove any 'f' after the number
if (c.charAt(end) == 'f') {
forceFloat = true;
end++;
}
// if its a negative, include the '-' sign
if (c.charAt(start-1) == '-') {
if (isNegativeSign(start-2, c)) {
start--;
}
}
// special case for ignoring (0x...). will be handled later
if (c.charAt(m.end()) == 'x' ||
c.charAt(m.end()) == 'X') {
continue;
}
// special case for ignoring number inside a string ("")
if (isInsideString(start, c))
continue;
// beware of the global assignment (bug from 26.07.2013)
if (isGlobal(m.start(), i))
continue;
int line = countLines(c.substring(0, start)) - 1; // zero based
String value = c.substring(start, end);
if (value.contains(".") || forceFloat) {
// consider this as a float
String name = varPrefix + "_float[" + floatVarCount +"]";
int decimalDigits = getNumDigitsAfterPoint(value);
handles.add(new Handle("float", name, floatVarCount, value, i, line, start, end, decimalDigits));
floatVarCount++;
} else {
// consider this as an int
String name = varPrefix + "_int[" + intVarCount +"]";
handles.add(new Handle("int", name, intVarCount, value, i, line, start, end, 0));
intVarCount++;
}
}
}
}
/**
* Get a list of all the hexadecimal numbers in the code
* @return
* list of all hexadecimal numbers in the sketch
*/
private void addAllHexNumbers() {
// for every number found:
// save its type (int/float), name, value and position in code.
Pattern p = Pattern.compile("[\\[\\{<>(),\\t\\s\\+\\-\\/\\*^%!|&=?:~]0x[A-Fa-f0-9]+");
for (int i = 0; i < codeTabs.length; i++) {
String c = codeTabs[i];
Matcher m = p.matcher(c);
while (m.find()) {
int start = m.start()+1;
int end = m.end();
if (isInRangeList(start, commentBlocks.get(i))) {
// ignore comments
continue;
}
if (isInRangeList(start, ignoreFunctions.get(i))) {
// ignore numbers in predefined functions
continue;
}
if (requiresComment) {
// only add numbers that have the "// tweak" comment in their line
if (!lineHasTweakComment(start, c)) {
continue;
}
}
// special case for ignoring number inside a string ("")
if (isInsideString(start, c)) {
continue;
}
// beware of the global assignment (bug from 26.07.2013)
if (isGlobal(m.start(), i)) {
continue;
}
int line = countLines(c.substring(0, start)) - 1; // zero based
String value = c.substring(start, end);
String name = varPrefix + "_int[" + intVarCount + "]";
Handle handle;
try {
handle = new Handle("hex", name, intVarCount, value, i, line, start, end, 0);
}
catch (NumberFormatException e) {
// don't add this number
continue;
}
allHandles.get(i).add(handle);
intVarCount++;
}
}
}
/**
* Get a list of all the webcolors (#) numbers in the code
* list of all hexadecimal numbers in the sketch
*/
private void addAllWebColorNumbers() {
Pattern p = Pattern.compile("#[A-Fa-f0-9]{6}");
for (int i=0; i findAllColorModes() {
ArrayList modes = new ArrayList();
for (int i=0; i -1) {
// found colorMode at index
if (isInRangeList(index, commentBlocks.get(i))) {
// ignore comments
continue;
}
index += 9;
int parOpen = tab.indexOf('(', index);
if (parOpen < 0) {
continue;
}
int parClose = tab.indexOf(')', parOpen+1);
if (parClose < 0) {
continue;
}
// add this mode
String modeDesc = tab.substring(parOpen+1, parClose);
String context = getObject(index-9, tab);
modes.add(ColorMode.fromString(context, modeDesc));
}
}
return modes;
}
private void createColorBoxes() {
colorBoxes = new ArrayList<>();
// search tab for the functions: 'color', 'fill', 'stroke', 'background', 'tint'
Pattern p = Pattern.compile("color\\(|color\\s\\(|fill[\\(\\s]|stroke[\\(\\s]|background[\\(\\s]|tint[\\(\\s]");
for (int i = 0; i < codeTabs.length; i++) {
List colorBox = new ArrayList();
colorBoxes.add(colorBox);
String tab = codeTabs[i];
Matcher m = p.matcher(tab);
while (m.find()) {
ArrayList colorHandles = new ArrayList();
// look for the '(' and ')' positions
int openPar = tab.indexOf("(", m.start());
int closePar = tab.indexOf(")", m.end());
if (openPar < 0 || closePar < 0) {
// ignore this color
continue;
}
if (isInRangeList(m.start(), commentBlocks.get(i))) {
// ignore colors in a comment
continue;
}
if (isInRangeList(m.start(), ignoreFunctions.get(i))) {
// ignore numbers in predefined functions
continue;
}
// look for handles inside the parenthesis
for (Handle handle : allHandles.get(i)) {
if (handle.startChar > openPar &&
handle.endChar <= closePar) {
// we have a match
colorHandles.add(handle);
}
}
if (colorHandles.size() > 0) {
/* make sure there is no other stuff between '()' like variables.
* subtract all handle values from string inside parenthesis and
* check there is no garbage left
*/
String insidePar = tab.substring(openPar+1, closePar);
for (Handle h : colorHandles) {
insidePar = insidePar.replaceFirst(h.strValue, "");
}
// make sure there is only ' ' and ',' left in the string.
boolean garbage = false;
for (int j=0; j.fill())
String context = getObject(m.start(), tab);
ColorMode cmode = getColorModeForContext(context);
// not adding color operations for modes we couldn't understand
ColorControlBox newCCB = new ColorControlBox(context, cmode, colorHandles);
if (cmode.unrecognizedMode) {
// the color mode is unrecognizable add only if is a hex or webcolor
if (newCCB.isHex) {
colorBox.add(newCCB);
}
} else {
colorBox.add(newCCB);
}
}
}
}
}
}
private void createColorBoxesForLights() {
// search code for light color and material color functions.
Pattern p = Pattern.compile("ambientLight[\\(\\s]|directionalLight[\\(\\s]"+
"|pointLight[\\(\\s]|spotLight[\\(\\s]|lightSpecular[\\(\\s]"+
"|specular[\\(\\s]|ambient[\\(\\s]|emissive[\\(\\s]");
for (int i=0; i colorHandles = new ArrayList();
// look for the '(' and ')' positions
int openPar = tab.indexOf("(", m.start());
int closePar = tab.indexOf(")", m.end());
if (openPar < 0 || closePar < 0) {
// ignore this color
continue;
}
if (isInRangeList(m.start(), commentBlocks.get(i))) {
// ignore colors in a comment
continue;
}
if (isInRangeList(m.start(), ignoreFunctions.get(i))) {
// ignore numbers in predefined functions
continue;
}
// put 'colorParamsEnd' after three parameters inside the parenthesis or at the close
int colorParamsEnd = openPar;
int commas=3;
while (commas-- > 0) {
colorParamsEnd=tab.indexOf(",", colorParamsEnd+1);
if (colorParamsEnd < 0 ||
colorParamsEnd > closePar) {
colorParamsEnd = closePar;
break;
}
}
for (Handle handle : allHandles.get(i)) {
if (handle.startChar > openPar &&
handle.endChar <= colorParamsEnd) {
// we have a match
colorHandles.add(handle);
}
}
if (colorHandles.size() > 0) {
/* make sure there is no other stuff between '()' like variables.
* subtract all handle values from string inside parenthesis and
* check there is no garbage left
*/
String insidePar = tab.substring(openPar+1, colorParamsEnd);
for (Handle h : colorHandles) {
insidePar = insidePar.replaceFirst(h.strValue, "");
}
// make sure there is only ' ' and ',' left in the string.
boolean garbage = false;
for (int j=0; j.fill())
String context = getObject(m.start(), tab);
ColorMode cmode = getColorModeForContext(context);
// not adding color operations for modes we couldn't understand
ColorControlBox newCCB = new ColorControlBox(context, cmode, colorHandles);
if (cmode.unrecognizedMode) {
// the color mode is unrecognizable add only if is a hex or webcolor
if (newCCB.isHex) {
colorBoxes.get(i).add(newCCB);
}
} else {
colorBoxes.get(i).add(newCCB);
}
}
}
}
}
}
private ColorMode getColorModeForContext(String context) {
for (ColorMode cm: colorModes) {
if (cm.drawContext.equals(context)) {
return cm;
}
}
// if none found, create the default color mode for this context and return it
ColorMode newMode = new ColorMode(context);
colorModes.add(newMode);
return newMode;
}
private void handleMultipleColorModes() {
// count how many color modes per context
Map modeCount = new HashMap();
for (ColorMode cm : colorModes) {
Integer prev = modeCount.get(cm.drawContext);
if (prev == null) {
prev = 0;
}
modeCount.put(cm.drawContext, prev+1);
}
// find the contexts that have more than one color mode
ArrayList multipleContexts = new ArrayList();
Set allContexts = modeCount.keySet();
for (String context : allContexts) {
if (modeCount.get(context) > 1) {
multipleContexts.add(context);
}
}
// keep only hex and web color boxes in color calls
// that belong to 'multipleContexts' contexts
for (int i = 0; i < codeTabs.length; i++) {
List toDelete = new ArrayList();
for (String context : multipleContexts) {
for (ColorControlBox ccb : colorBoxes.get(i)) {
if (ccb.drawContext.equals(context) && !ccb.isHex) {
toDelete.add(ccb);
}
}
}
colorBoxes.get(i).removeAll(toDelete);
}
}
private List> getAllScientificNotations() {
List> notations = new ArrayList<>();
Pattern p = Pattern.compile("[+\\-]?(?:0|[1-9]\\d*)(?:\\.\\d*)?[eE][+\\-]?\\d+");
for (String code : codeTabs) {
List notation = new ArrayList();
Matcher m = p.matcher(code);
while (m.find()) {
notation.add(new Range(m.start(), m.end()));
}
notations.add(notation);
}
return notations;
}
static public boolean containsTweakComment(String[] codeTabs) {
for (String tab : codeTabs) {
if (hasTweakComment(tab)) {
return true;
}
}
return false;
}
static private boolean lineHasTweakComment(int pos, String code) {
int lineEnd = getEndOfLine(pos, code);
if (lineEnd < 0) {
return false;
}
String line = code.substring(pos, lineEnd);
return hasTweakComment(line);
}
static private boolean hasTweakComment(String code) {
// https://github.com/processing/processing/issues/3742
return code.contains("/// tweak");
/*
Pattern p = Pattern.compile("\\/\\/.*tweak", Pattern.CASE_INSENSITIVE);
Matcher m = p.matcher(code);
return m.find();
*/
}
static private boolean isNegativeSign(int pos, String code) {
// go back and look for ,{[(=?+-/*%<>:&|^!~
for (int i = pos; i >= 0; i--) {
char c = code.charAt(i);
if (c != ' ' && c != '\t') {
return (c==',' || c=='{' || c=='[' || c=='(' ||
c=='=' || c=='?' || c=='+' || c=='-' ||
c=='/' || c=='*' || c=='%' || c=='<' ||
c=='>' || c==':' || c=='&' || c=='|' ||
c=='^' || c=='!' || c=='~');
}
}
return false;
}
static private int getNumDigitsAfterPoint(String number) {
Pattern p = Pattern.compile("\\.[0-9]+");
Matcher m = p.matcher(number);
if (m.find()) {
return m.end() - m.start() - 1;
}
return 0;
}
static private int countLines(String str) {
String[] lines = str.split("\r\n|\n\r|\n|\r");
return lines.length;
}
/**
* Are we inside a string? (TODO: ignore comments in the code)
* @param pos
* position in the code
* @param code
* the code
* @return
*/
static private boolean isInsideString(int pos, String code) {
int quoteNum = 0; // count '"'
for (int c = pos; c>=0 && code.charAt(c) != '\n'; c--) {
if (code.charAt(c) == '"') {
quoteNum++;
}
}
if (quoteNum%2 == 1) {
return true;
}
return false;
}
/**
* Builds an int array for every tab that represents
* the scope depth at each character.
*/
static private int[] getCurlyScopes(String code) {
List comments = getCommentBlocks(code);
int[] scopes = new int[code.length()];
int curlyScope = 0;
boolean arrayAssignmentMaybeCommingFlag = false;
int arrayAssignmentCurlyScope = 0;
for (int pos=0; pos0) {
// this is an array assignment
arrayAssignmentCurlyScope++;
arrayAssignmentMaybeCommingFlag = false;
}
else {
curlyScope++;
}
}
else if (code.charAt(pos) == '}') {
if (arrayAssignmentCurlyScope>0) {
arrayAssignmentCurlyScope--;
}
else {
curlyScope--;
}
}
else if (code.charAt(pos) == '=') {
arrayAssignmentMaybeCommingFlag = true;
}
else if (!isWhiteSpace(code.charAt(pos))) {
arrayAssignmentMaybeCommingFlag = false;
}
}
return scopes;
}
static private boolean isWhiteSpace(char c) {
return c == ' ' || c == '\t' || c == '\n' || c == '\r';
}
/**
* Is this a global position?
* @param pos position
* @param codeTabIndex index of the code in codeTabs
* @return true if the position 'pos' is in global scope
* in the code 'codeTabs[codeTabIndex]'
*
*/
private boolean isGlobal(int pos, int codeTabIndex) {
return (curlyScopes.get(codeTabIndex)[pos]==0);
};
static private List getCommentBlocks(String code) {
List commentBlocks = new ArrayList();
int lastBlockStart=0;
boolean lookForEnd = false;
for (int pos=0; pos rangeList) {
for (Range r : rangeList) {
if (r.contains(pos)) {
return true;
}
}
return false;
}
static private int getEndOfLine(int pos, String code) {
return code.indexOf("\n", pos);
}
/**
* returns the object name (what comes before the '.')
* of the function starting at 'pos'
*/
static private String getObject(int pos, String code) {
boolean readObject = false;
String obj = "this";
while (pos-- >= 0) {
if (code.charAt(pos) == '.') {
if (!readObject) {
obj = "";
readObject = true;
}
else {
break;
}
}
else if (code.charAt(pos) == ' ' || code.charAt(pos) == '\t') {
break;
}
else if (readObject) {
obj = code.charAt(pos) + obj;
}
}
return obj;
}
static public Range getVoidFunctionRange(String code, String functionName) {
return new Range(getVoidFunctionStart(code, functionName),
getVoidFunctionEnd(code, functionName));
}
static public int getVoidFunctionStart(String code, String functionName) {
Pattern p = Pattern.compile("void[\\s\\t\\r\\n]*"+functionName+"[\\s\\t]*\\(\\)[\\s\\t\\r\\n]*\\{");
Matcher m = p.matcher(code);
if (m.find()) {
return m.end();
}
return -1;
}
static public int getVoidFunctionEnd(String code, String functionName) {
List comments = getCommentBlocks(code);
int start = getVoidFunctionStart(code, functionName);
if (start == -1) {
return -1;
}
// count brackets to look for setup end
int bracketCount=1;
int pos = start;
while (bracketCount > 0 && pos < code.length()) {
if (isInRangeList(pos, comments)) {
// in a comment, ignore and move on
pos++;
continue;
}
if (code.charAt(pos) == '{') {
bracketCount++;
}
else if (code.charAt(pos) == '}') {
bracketCount--;
}
pos++;
}
if (bracketCount == 0) {
return pos-1;
}
return -1;
}
static public int getSetupStart(String code) {
Pattern p = Pattern.compile("void[\\s\\t\\r\\n]*setup[\\s\\t]*\\(\\)[\\s\\t\\r\\n]*\\{");
Matcher m = p.matcher(code);
if (m.find()) {
return m.end();
}
return -1;
}
static public int getSetupEnd(String code) {
List comments = getCommentBlocks(code);
int setupStart = getSetupStart(code);
if (setupStart == -1) {
return -1;
}
// count brackets to look for setup end
int bracketCount=1;
int pos = setupStart;
while (bracketCount > 0 && pos < code.length()) {
if (isInRangeList(pos, comments)) {
// in a comment, ignore and move on
pos++;
continue;
}
if (code.charAt(pos) == '{') {
bracketCount++;
}
else if (code.charAt(pos) == '}') {
bracketCount--;
}
pos++;
}
if (bracketCount == 0) {
return pos-1;
}
return -1;
}
static public int getAfterSizePos(String code) {
List comments = getCommentBlocks(code);
// find the size function
Pattern p = Pattern.compile("size[\\s\\t]*\\(");
Matcher m = p.matcher(code);
while (m.find()) {
if (isInRangeList(m.start(), comments) ||
isInRangeList(m.end(), comments)) {
// this is a comment, next
continue;
}
// count brackets to look for size call end
int bracketCount=1;
int pos = m.end();
while (bracketCount > 0 && pos < code.length()) {
if (isInRangeList(pos, comments)) {
// in a comment, ignore and move on
pos++;
continue;
}
if (code.charAt(pos) == '(') {
bracketCount++;
}
else if (code.charAt(pos) == ')') {
bracketCount--;
}
pos++;
}
if (bracketCount != 0) {
// could not find closing ')'. next
continue;
}
// find ';' sign
boolean found = false;
while (pos < code.length()) {
if (code.charAt(pos) == ';' &&
!isInRangeList(pos, comments)) {
found = true;
break;
}
pos++;
}
if (!found) {
// didn't find the ';'. next
continue;
}
// success! we found the place
return pos+1;
}
// nothing was found
return -1;
}
static class Range {
int start;
int end;
Range(int s, int e) {
start = s;
end = e;
}
boolean contains(int v) {
return v >= start && v < end;
}
}
}