processing.mode.java.preproc.PdePreprocessor 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 -*- */
/*
PdePreprocessor - wrapper for default ANTLR-generated parser
Part of the Processing project - http://processing.org
Copyright (c) 2004-15 Ben Fry and Casey Reas
Copyright (c) 2001-04 Massachusetts Institute of Technology
ANTLR-generated parser and several supporting classes written
by Dan Mosedale via funding from the Interaction Institute IVREA.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package processing.mode.java.preproc;
import java.io.*;
import java.util.*;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import processing.app.Messages;
import processing.app.Preferences;
import processing.app.SketchException;
import processing.core.PApplet;
import processing.data.StringList;
import antlr.*;
import antlr.collections.AST;
/**
* Class that orchestrates preprocessing p5 syntax into straight Java.
*
* Current Preprocessor Subsitutions:
*
* - any function not specified as being protected or private will
* be made 'public'. this means that void setup() becomes
* public void setup(). This is important to note when
* coding with core.jar outside of the PDE.
*
- compiler.substitute_floats (currently "substitute_f")
* treat doubles as floats, i.e. 12.3 becomes 12.3f so that people
* don't have to add f after their numbers all the time since it's
* confusing for beginners.
*
- compiler.enhanced_casting byte(), char(), int(), float()
* works for casting. this is basic in the current implementation, but
* should be expanded as described above. color() works similarly to int(),
* however there is also a *function* called color(r, g, b) in p5.
*
- compiler.color_datatype 'color' is aliased to 'int'
* as a datatype to represent ARGB packed into a single int, commonly
* used in p5 for pixels[] and other color operations. this is just a
* search/replace type thing, and it can be used interchangeably with int.
*
- compiler.web_colors (currently "inline_web_colors")
* color c = #cc0080; should unpack to 0xffcc0080 (the ff at the top is
* so that the color is opaque), which is just an int.
*
* Other preprocessor functionality
*
* - detects what 'mode' the program is in: static (no function
* brackets at all, just assumes everything is in draw), active
* (setup plus draw or loop), and java mode (full java support).
* http://processing.org/reference/environment/
*
*
* The PDE Preprocessor is based on the Java Grammar that comes with
* ANTLR 2.7.2. Moving it forward to a new version of the grammar
* shouldn't be too difficult.
*
* Here's some info about the various files in this directory:
*
* java.g: this is the ANTLR grammar for Java 1.3/1.4 from the
* ANTLR distribution. It is in the public domain. The only change to
* this file from the original this file is the uncommenting of the
* clauses required to support assert().
*
* java.tree.g: this describes the Abstract Syntax Tree (AST)
* generated by java.g. It is only here as a reference for coders hacking
* on the preprocessor, it is not built or used at all. Note that pde.g
* overrides some of the java.g rules so that in PDE ASTs, there are a
* few minor differences. Also in the public domain.
*
* pde.g: this is the grammar and lexer for the PDE language
* itself. It subclasses the java.g grammar and lexer. There are a couple
* of overrides to java.g that I hope to convince the ANTLR folks to fold
* back into their grammar, but most of this file is highly specific to
* PDE itself.
* PdeEmitter.java: this class traverses the AST generated by
* the PDE Recognizer, and emits it as Java code, doing any necessary
* transformations along the way. It is based on JavaEmitter.java,
* available from antlr.org, written by Andy Tripp ,
* who has given permission for it to be distributed under the GPL.
*
* ExtendedCommonASTWithHiddenTokens.java: this adds a necessary
* initialize() method, as well as a number of methods to allow for XML
* serialization of the parse tree in a such a way that the hidden tokens
* are visible. Much of the code is taken from the original
* CommonASTWithHiddenTokens class. I hope to convince the ANTLR folks
* to fold these changes back into that class so that this file will be
* unnecessary.
*
* TokenStreamCopyingHiddenTokenFilter.java: this class provides
* TokenStreamHiddenTokenFilters with the concept of tokens which can be
* copied so that they are seen by both the hidden token stream as well
* as the parser itself. This is useful when one wants to use an
* existing parser (like the Java parser included with ANTLR) that throws
* away some tokens to create a parse tree which can be used to spit out
* a copy of the code with only minor modifications. Partially derived
* from ANTLR code. I hope to convince the ANTLR folks to fold this
* functionality back into ANTLR proper as well.
*
* whitespace_test.pde: a torture test to ensure that the
* preprocessor is correctly preserving whitespace, comments, and other
* hidden tokens correctly. See the comments in the code for details about
* how to run the test.
*
* All other files in this directory are generated at build time by ANTLR
* itself. The ANTLR manual goes into a fair amount of detail about the
* what each type of file is for.
*
*/
public class PdePreprocessor {
protected static final String UNICODE_ESCAPES = "0123456789abcdefABCDEF";
// used for calling the ASTFactory to get the root node
private static final int ROOT_ID = 0;
protected final String indent;
private final String name;
public enum Mode {
STATIC, ACTIVE, JAVA
}
private TokenStreamCopyingHiddenTokenFilter filter;
private String advClassName = "";
protected Mode mode;
Set foundMethods;
SurfaceInfo sizeInfo;
/**
* Regular expression for parsing the size() method. This should match
* against any uses of the size() function, whether numbers or variables
* or whatever. This way, no warning is shown if size() isn't actually used
* in the sketch, which is the case especially for anyone who is cutting
* and pasting from the reference.
*/
// public static final String SIZE_REGEX =
// "(?:^|\\s|;)size\\s*\\(\\s*([^\\s,]+)\\s*,\\s*([^\\s,\\)]+)\\s*,?\\s*([^\\)]*)\\s*\\)\\s*\\;";
// static private final String SIZE_CONTENTS_REGEX =
// "(?:^|\\s|;)size\\s*\\(([^\\)]+)\\)\\s*\\;";
// static private final String FULL_SCREEN_CONTENTS_REGEX =
// "(?:^|\\s|;)fullScreen\\s*\\(([^\\)]+)\\)\\s*\\;";
// /** Test whether there's a void somewhere (the program has functions). */
// static private final String VOID_REGEX =
// "(?:^|\\s|;)void\\s";
/** Used to grab the start of setup() so we can mine it for size() */
static private final Pattern VOID_SETUP_REGEX =
Pattern.compile("(?:^|\\s|;)void\\s+setup\\s*\\(", Pattern.MULTILINE);
// Can't only match any 'public class', needs to be a PApplet
// http://code.google.com/p/processing/issues/detail?id=551
static private final Pattern PUBLIC_CLASS =
Pattern.compile("(^|;)\\s*public\\s+class\\s+\\S+\\s+extends\\s+PApplet", Pattern.MULTILINE);
static private final Pattern FUNCTION_DECL =
Pattern.compile("(^|;)\\s*((public|private|protected|final|static)\\s+)*" +
"(void|int|float|double|String|char|byte|boolean)" +
"(\\s*\\[\\s*\\])?\\s+[a-zA-Z0-9]+\\s*\\(",
Pattern.MULTILINE);
static private final Pattern CLOSING_BRACE = Pattern.compile("\\}");
public PdePreprocessor(final String sketchName) {
this(sketchName, Preferences.getInteger("editor.tabs.size"));
}
public PdePreprocessor(final String sketchName, final int tabSize) {
this.name = sketchName;
final char[] indentChars = new char[tabSize];
Arrays.fill(indentChars, ' ');
indent = new String(indentChars);
}
public SurfaceInfo initSketchSize(String code,
boolean sizeWarning) throws SketchException {
sizeInfo = parseSketchSize(code, sizeWarning);
return sizeInfo;
}
/**
* Break on commas, except those inside quotes,
* e.g.: size(300, 200, PDF, "output,weirdname.pdf");
* No special handling implemented for escaped (\") quotes.
*/
static private StringList breakCommas(String contents) {
StringList outgoing = new StringList();
boolean insideQuote = false;
// The current word being read
StringBuilder current = new StringBuilder();
char[] chars = contents.toCharArray();
for (int i = 0; i < chars.length; i++) {
char c = chars[i];
if (insideQuote) {
current.append(c);
if (c == '\"') {
insideQuote = false;
}
} else {
if (c == ',') {
if (current.length() != 0) {
outgoing.append(current.toString());
current.setLength(0);
}
} else {
current.append(c);
if (c == '\"') {
insideQuote = true;
}
}
}
}
if (current.length() != 0) {
outgoing.append(current.toString());
}
return outgoing;
}
/**
* Parse a chunk of code and extract the size() command and its contents.
* Also goes after fullScreen(), smooth(), and noSmooth().
* @param code The code from the main tab in the sketch
* @param fussy true if it should show an error message if bad size()
* @return null if there was an error, otherwise an array (might contain some/all nulls)
*/
static public SurfaceInfo parseSketchSize(String code,
boolean fussy) throws SketchException {
// This matches against any uses of the size() function, whether numbers
// or variables or whatever. This way, no warning is shown if size() isn't
// actually used in the applet, which is the case especially for anyone
// who is cutting/pasting from the reference.
// String scrubbed = scrubComments(sketch.getCode(0).getProgram());
// String[] matches = PApplet.match(scrubbed, SIZE_REGEX);
// String[] matches = PApplet.match(scrubComments(code), SIZE_REGEX);
/*
1. no size() or fullScreen() method at all
will use the non-overridden settings() method in PApplet
2. size() or fullScreen() found inside setup() (static mode sketch or otherwise)
make sure that it uses numbers (or displayWidth/Height), copy into settings
3. size() or fullScreen() already in settings()
don't mess with the sketch, don't insert any defaults
really only need to deal with situation #2.. nothing to be done for 1 and 3
*/
// if static mode sketch, all we need is regex
// easy proxy for static in this case is whether [^\s]void\s is present
String uncommented = scrubComments(code);
Mode mode = parseMode(uncommented);
String searchArea = null;
switch (mode) {
case JAVA:
// it's up to the user
searchArea = null;
break;
case ACTIVE:
// active mode, limit scope to setup
// Find setup() in global scope
MatchResult setupMatch = findInCurrentScope(VOID_SETUP_REGEX, uncommented);
if (setupMatch != null) {
int start = uncommented.indexOf("{", setupMatch.end());
if (start >= 0) {
// Find a closing brace
MatchResult match = findInCurrentScope(CLOSING_BRACE, uncommented, start);
if (match != null) {
searchArea = uncommented.substring(start + 1, match.end() - 1);
} else {
throw new SketchException("Found a { that's missing a matching }", false);
}
}
}
break;
case STATIC:
// static mode, look everywhere
searchArea = uncommented;
break;
}
if (searchArea == null) {
return new SurfaceInfo();
}
StringList extraStatements = new StringList();
// First look for noSmooth() or smooth(N) so we can hoist it into settings.
String[] smoothContents = matchMethod("smooth", searchArea);
if (smoothContents != null) {
extraStatements.append(smoothContents[0]);
}
String[] noContents = matchMethod("noSmooth", searchArea);
if (noContents != null) {
if (extraStatements.size() != 0) {
throw new SketchException("smooth() and noSmooth() cannot be used in the same sketch");
} else {
extraStatements.append(noContents[0]);
}
}
String[] pixelDensityContents = matchMethod("pixelDensity", searchArea);
if (pixelDensityContents != null) {
extraStatements.append(pixelDensityContents[0]);
} else {
pixelDensityContents = matchDensityMess(searchArea);
if (pixelDensityContents != null) {
extraStatements.append(pixelDensityContents[0]);
}
}
String[] sizeContents = matchMethod("size", searchArea);
String[] fullContents = matchMethod("fullScreen", searchArea);
// First check and make sure they aren't both being used, otherwise it'll
// throw a confusing state exception error that one "can't be used here".
if (sizeContents != null && fullContents != null) {
throw new SketchException("size() and fullScreen() cannot be used in the same sketch", false);
}
// Get everything inside the parens for the size() method
//String[] contents = PApplet.match(searchArea, SIZE_CONTENTS_REGEX);
if (sizeContents != null) {
StringList args = breakCommas(sizeContents[1]);
SurfaceInfo info = new SurfaceInfo();
// info.statement = sizeContents[0];
info.addStatement(sizeContents[0]);
info.width = args.get(0).trim();
info.height = args.get(1).trim();
info.renderer = (args.size() >= 3) ? args.get(2).trim() : null;
info.path = (args.size() >= 4) ? args.get(3).trim() : null;
// Trying to remember why we wanted to allow people to use displayWidth
// as the height or displayHeight as the width, but maybe it's for
// making a square sketch window? Not going to
if (info.hasOldSyntax()) {
// return null;
throw new SketchException("Please update your code to continue.", false);
}
if (info.hasBadSize() && fussy) {
// found a reference to size, but it didn't seem to contain numbers
final String message =
"The size of this sketch could not be determined from your code.\n" +
"Use only numbers (not variables) for the size() command.\n" +
"Read the size() reference for more details.";
Messages.showWarning("Could not find sketch size", message, null);
// new Exception().printStackTrace(System.out);
// return null;
throw new SketchException("Please fix the size() line to continue.", false);
}
info.addStatements(extraStatements);
info.checkEmpty();
return info;
//return new String[] { contents[0], width, height, renderer, path };
}
// if no size() found, check for fullScreen()
//contents = PApplet.match(searchArea, FULL_SCREEN_CONTENTS_REGEX);
if (fullContents != null) {
SurfaceInfo info = new SurfaceInfo();
// info.statement = fullContents[0];
info.addStatement(fullContents[0]);
StringList args = breakCommas(fullContents[1]);
if (args.size() > 0) { // might have no args
String args0 = args.get(0).trim();
if (args.size() == 1) {
// could be either fullScreen(1) or fullScreen(P2D), figure out which
if (args0.equals("SPAN") || PApplet.parseInt(args0, -1) != -1) {
// it's the display parameter, not the renderer
info.display = args0;
} else {
info.renderer = args0;
}
} else if (args.size() == 2) {
info.renderer = args0;
info.display = args.get(1).trim();
} else {
throw new SketchException("That's too many parameters for fullScreen()");
}
}
info.width = "displayWidth";
info.height = "displayHeight";
// if (extraStatements.size() != 0) {
// info.statement += extraStatements.join(" ");
// }
info.addStatements(extraStatements);
info.checkEmpty();
return info;
}
// Made it this far, but no size() or fullScreen(), and still
// need to pull out the noSmooth() and smooth(N) methods.
if (extraStatements.size() != 0) {
SurfaceInfo info = new SurfaceInfo();
// info.statement = extraStatements.join(" ");
info.addStatements(extraStatements);
return info;
}
// not an error, just no size() specified
//return new String[] { null, null, null, null, null };
return new SurfaceInfo();
}
/*
static String readSingleQuote(char[] c, int i) {
StringBuilder sb = new StringBuilder();
try {
sb.append(c[i++]); // add the quote
if (c[i] == '\\') {
sb.append(c[i++]); // add the escape
if (c[i] == 'u') {
// grabs uNNN and the fourth N will be added below
for (int j = 0; j < 4; j++) {
sb.append(c[i++]);
}
}
}
sb.append(c[i++]); // get the char, escapee, or last unicode digit
sb.append(c[i++]); // get the closing quote
} catch (ArrayIndexOutOfBoundsException ignored) {
// this means they have bigger problems with their code
}
return sb.toString();
}
static String readDoubleQuote(char[] c, int i) {
StringBuilder sb = new StringBuilder();
try {
sb.append(c[i++]); // add the quote
while (i < c.length) {
if (c[i] == '\\') {
sb.append(c[i++]); // add the escape
sb.append(c[i++]); // add whatever was escaped
} else if (c[i] == '\"') {
sb.append(c[i++]);
break;
} else {
sb.append(c[i++]);
}
}
} catch (ArrayIndexOutOfBoundsException ignored) {
// this means they have bigger problems with their code
}
return sb.toString();
}
*/
/**
* Parses the code and determines the mode of the sketch.
*
* @param code code without comments
* @return determined mode
*/
static public Mode parseMode(CharSequence code) {
// See if we can find any function in the global scope
if (findInCurrentScope(FUNCTION_DECL, code) != null) {
return Mode.ACTIVE;
}
// See if we can find any public class extending PApplet
if (findInCurrentScope(PUBLIC_CLASS, code) != null) {
return Mode.JAVA;
}
return Mode.STATIC;
}
/**
* Calls {@link #findInScope(Pattern, String, int, int, int, int) findInScope}
* on the whole string with min and max target scopes set to zero.
*/
static protected MatchResult findInCurrentScope(Pattern pattern, CharSequence code) {
return findInScope(pattern, code, 0, code.length(), 0, 0);
}
/**
* Calls {@link #findInScope(Pattern, String, int, int, int, int) findInScope}
* starting at start char with min and max target scopes set to zero.
*/
static protected MatchResult findInCurrentScope(Pattern pattern, CharSequence code,
int start) {
return findInScope(pattern, code, start, code.length(), 0, 0);
}
/**
* Looks for the pattern at a specified target scope depth relative
* to the scope depth of the starting position.
*
* Example: Calling this with starting position inside a method body
* and target depth 0 would search only in the method body, while
* using target depth -1 would look only in the body of the enclosing class
* (but not in any methods of the class or outside of the class).
*
* By using a scope range, you can e.g. search in the whole class including
* bodies of methods and inner classes.
*
* @param pattern matching is realized by find() method of this pattern
* @param code Java code without comments
* @param start starting position in the code String (inclusive)
* @param stop ending position in the code Sting (exclusive)
* @param minTargetScopeDepth desired min scope depth of the match relative to the
* scope of the starting position
* @param maxTargetScopeDepth desired max scope depth of the match relative to the
* scope of the starting position
* @return first match at a desired relative scope depth,
* null if there isn't one
*/
static protected MatchResult findInScope(Pattern pattern, CharSequence code,
int start, int stop,
int minTargetScopeDepth,
int maxTargetScopeDepth) {
if (minTargetScopeDepth > maxTargetScopeDepth) {
int temp = minTargetScopeDepth;
minTargetScopeDepth = maxTargetScopeDepth;
maxTargetScopeDepth = temp;
}
Matcher m = pattern.matcher(code);
m.region(start, stop);
int depth = 0;
int position = start;
// We should not escape the enclosing scope. It can be either the original
// scope, or the min target scope, whichever is more out there (lower depth)
int minScopeDepth = PApplet.min(depth, minTargetScopeDepth);
while (m.find()) {
int newPosition = m.end();
int depthDiff = scopeDepthDiff(code, position, newPosition);
// Process this match only if it is not in string or char literal
if (depthDiff != Integer.MAX_VALUE) {
depth += depthDiff;
if (depth < minScopeDepth) break; // out of scope!
if (depth >= minTargetScopeDepth &&
depth <= maxTargetScopeDepth) {
return m.toMatchResult(); // jackpot
}
position = newPosition;
}
}
return null;
}
/**
* Walks the specified region (not including stop) and determines difference
* in scope depth. Adds one to depth on opening curly brace, subtracts one
* from depth on closing curly brace. Ignores string and char literals.
*
* @param code code without comments
* @param start start of the region, must not be in string literal,
* char literal or second char of escaped sequence
* @param stop end of the region (exclusive)
*
* @return scope depth difference between start and stop,
* Integer.MAX_VALUE if end is in string literal,
* char literal or second char of escaped sequence
*/
static protected int scopeDepthDiff(CharSequence code, int start, int stop) {
boolean insideString = false;
boolean insideChar = false;
boolean escapedChar = false;
int depth = 0;
for (int i = start; i < stop; i++) {
if (!escapedChar) {
char ch = code.charAt(i);
switch (ch) {
case '\\':
escapedChar = true;
break;
case '{':
if (!insideChar && !insideString) depth++;
break;
case '}':
if (!insideChar && !insideString) depth--;
break;
case '\"':
if (!insideChar) insideString = !insideString;
break;
case '\'':
if (!insideString) insideChar = !insideChar;
break;
}
} else {
escapedChar = false;
}
}
if (insideChar || insideString || escapedChar) {
return Integer.MAX_VALUE; // signal invalid location
}
return depth;
}
/**
* Looks for the specified method in the base scope of the search area.
*/
static protected String[] matchMethod(String methodName, String searchArea) {
final String left = "(?:^|\\s|;)";
// doesn't match empty pairs of parens
//final String right = "\\s*\\(([^\\)]+)\\)\\s*;";
final String right = "\\s*\\(([^\\)]*)\\)\\s*;";
String regexp = left + methodName + right;
Pattern p = matchPatterns.get(regexp);
if (p == null) {
p = Pattern.compile(regexp, Pattern.MULTILINE | Pattern.DOTALL);
matchPatterns.put(regexp, p);
}
MatchResult match = findInCurrentScope(p, searchArea);
if (match != null) {
int count = match.groupCount() + 1;
String[] groups = new String[count];
for (int i = 0; i < count; i++) {
groups[i] = match.group(i);
}
return groups;
}
return null;
}
static protected LinkedHashMap matchPatterns =
new LinkedHashMap(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
// Limit the number of match patterns at 10 most recently used
return size() == 10;
}
};
static protected String[] matchDensityMess(String searchArea) {
final String regexp =
"(?:^|\\s|;)pixelDensity\\s*\\(\\s*displayDensity\\s*\\([^\\)]*\\)\\s*\\)\\s*\\;";
return PApplet.match(searchArea, regexp);
}
/**
* Replace all commented portions of a given String as spaces.
* Utility function used here and in the preprocessor.
*/
static public String scrubComments(String what) {
char p[] = what.toCharArray();
// Track quotes to avoid problems with code like: String t = "*/*";
// http://code.google.com/p/processing/issues/detail?id=1435
boolean insideQuote = false;
int index = 0;
while (index < p.length) {
// for any double slash comments, ignore until the end of the line
if (!insideQuote &&
(p[index] == '/') &&
(index < p.length - 1) &&
(p[index+1] == '/')) {
p[index++] = ' ';
p[index++] = ' ';
while ((index < p.length) &&
(p[index] != '\n')) {
p[index++] = ' ';
}
// check to see if this is the start of a new multiline comment.
// if it is, then make sure it's actually terminated somewhere.
} else if (!insideQuote &&
(p[index] == '/') &&
(index < p.length - 1) &&
(p[index+1] == '*')) {
p[index++] = ' ';
p[index++] = ' ';
boolean endOfRainbow = false;
while (index < p.length - 1) {
if ((p[index] == '*') && (p[index+1] == '/')) {
p[index++] = ' ';
p[index++] = ' ';
endOfRainbow = true;
break;
} else {
// continue blanking this area
p[index++] = ' ';
}
}
if (!endOfRainbow) {
throw new RuntimeException("Missing the */ from the end of a " +
"/* comment */");
}
// switch in/out of quoted region
} else if (p[index] == '"') {
insideQuote = !insideQuote;
index++;
// skip the escaped char
} else if (insideQuote && p[index] == '\\') {
index += 2;
} else { // any old character, move along
index++;
}
}
return new String(p);
}
public void addMethod(String methodName) {
foundMethods.add(methodName);
}
public boolean hasMethod(String methodName) {
return foundMethods.contains(methodName);
}
// public void setFoundMain(boolean foundMain) {
// this.foundMain = foundMain;
// }
// public boolean getFoundMain() {
// return foundMain;
// }
public void setAdvClassName(final String advClassName) {
this.advClassName = advClassName;
}
public void setMode(final Mode mode) {
//System.err.println("Setting mode to " + mode);
this.mode = mode;
}
CommonHiddenStreamToken getHiddenAfter(final CommonHiddenStreamToken t) {
return filter.getHiddenAfter(t);
}
CommonHiddenStreamToken getInitialHiddenToken() {
return filter.getInitialHiddenToken();
}
private static int countNewlines(final String s) {
int count = 0;
for (int pos = s.indexOf('\n', 0); pos >= 0; pos = s.indexOf('\n', pos + 1))
count++;
return count;
}
private static void checkForUnterminatedMultilineComment(final String program)
throws SketchException {
final int length = program.length();
for (int i = 0; i < length; i++) {
// for any double slash comments, ignore until the end of the line
if ((program.charAt(i) == '/') && (i < length - 1)
&& (program.charAt(i + 1) == '/')) {
i += 2;
while ((i < length) && (program.charAt(i) != '\n')) {
i++;
}
// check to see if this is the start of a new multiline comment.
// if it is, then make sure it's actually terminated somewhere.
} else if ((program.charAt(i) == '/') && (i < length - 1)
&& (program.charAt(i + 1) == '*')) {
final int startOfComment = i;
i += 2;
boolean terminated = false;
while (i < length - 1) {
if ((program.charAt(i) == '*') && (program.charAt(i + 1) == '/')) {
i++; // advance to the ending '/'
terminated = true;
break;
} else {
i++;
}
}
if (!terminated) {
throw new SketchException("Unclosed /* comment */", 0,
countNewlines(program.substring(0,
startOfComment)));
}
} else if (program.charAt(i) == '"') {
final int stringStart = i;
boolean terminated = false;
for (i++; i < length; i++) {
final char c = program.charAt(i);
if (c == '"') {
terminated = true;
break;
} else if (c == '\\') {
if (i == length - 1) {
break;
}
i++;
} else if (c == '\n') {
break;
}
}
if (!terminated) {
throw new SketchException("Unterminated string constant", 0,
countNewlines(program.substring(0,
stringStart)));
}
} else if (program.charAt(i) == '\'') {
i++; // step over the initial quote
if (i >= length) {
throw new SketchException("Unterminated character constant (after initial quote)", 0,
countNewlines(program.substring(0, i)));
}
boolean escaped = false;
if (program.charAt(i) == '\\') {
i++; // step over the backslash
escaped = true;
}
if (i >= length) {
throw new SketchException("Unterminated character constant (after backslash)", 0,
countNewlines(program.substring(0, i)));
}
if (escaped && program.charAt(i) == 'u') { // unicode escape sequence?
i++; // step over the u
//i += 4; // and the four digit unicode constant
for (int j = 0; j < 4; j++) {
if (UNICODE_ESCAPES.indexOf(program.charAt(i)) == -1) {
throw new SketchException("Bad or unfinished \\uXXXX sequence " +
"(malformed Unicode character constant)", 0,
countNewlines(program.substring(0, i)));
}
i++;
}
} else {
i++; // step over a single character
}
if (i >= length) {
throw new SketchException("Unterminated character constant", 0,
countNewlines(program.substring(0, i)));
}
if (program.charAt(i) != '\'') {
throw new SketchException("Badly formed character constant " +
"(expecting quote, got " + program.charAt(i) + ")", 0,
countNewlines(program.substring(0, i)));
}
}
}
}
public PreprocessorResult write(final Writer out, String program)
throws SketchException, RecognitionException, TokenStreamException {
return write(out, program, null);
}
public PreprocessorResult write(Writer out, String program,
StringList codeFolderPackages)
throws SketchException, RecognitionException, TokenStreamException {
// these ones have the .* at the end, since a class name might be at the end
// instead of .* which would make trouble other classes using this can lop
// off the . and anything after it to produce a package name consistently.
final ArrayList programImports = new ArrayList();
// imports just from the code folder, treated differently
// than the others, since the imports are auto-generated.
final ArrayList codeFolderImports = new ArrayList();
// need to reset whether or not this has a main()
// foundMain = false;
foundMethods = new HashSet();
// http://processing.org/bugs/bugzilla/5.html
if (!program.endsWith("\n")) {
program += "\n";
}
checkForUnterminatedMultilineComment(program);
if (Preferences.getBoolean("preproc.substitute_unicode")) {
program = substituteUnicode(program);
}
// For 0215, adding } as a legitimate prefix to the import (along with
// newline and semicolon) for cases where a tab ends with } and an import
// statement starts the next tab.
final String importRegexp =
"((?:^|;|\\})\\s*)(import\\s+)((?:static\\s+)?\\S+)(\\s*;)";
final Pattern importPattern = Pattern.compile(importRegexp);
String scrubbed = scrubComments(program);
Matcher m = null;
int offset = 0;
boolean found = false;
do {
m = importPattern.matcher(scrubbed);
found = m.find(offset);
if (found) {
// System.out.println("found " + m.groupCount() + " groups");
String before = m.group(1);
String piece = m.group(2) + m.group(3) + m.group(4);
// int len = piece.length(); // how much to trim out
if (!ignoreImport(m.group(3))) {
programImports.add(m.group(3)); // the package name
}
// find index of this import in the program
int start = m.start() + before.length();
int stop = start + piece.length();
// System.out.println(start + " " + stop + " " + piece);
//System.out.println("found " + m.group(3));
// System.out.println("removing '" + program.substring(start, stop) + "'");
// Remove the import from the main program
program = program.substring(0, start) + program.substring(stop);
scrubbed = scrubbed.substring(0, start) + scrubbed.substring(stop);
// Set the offset to start, because everything between
// start and stop has been deleted.
offset = m.start();
}
} while (found);
// System.out.println("program now:");
// System.out.println(program);
if (codeFolderPackages != null) {
for (String item : codeFolderPackages) {
codeFolderImports.add(item + ".*");
}
}
final PrintWriter stream = new PrintWriter(out);
final int headerOffset =
writeImports(stream, programImports, codeFolderImports);
return new PreprocessorResult(mode, headerOffset + 2,
write(program, stream), programImports);
}
static String substituteUnicode(String program) {
// check for non-ascii chars (these will be/must be in unicode format)
char p[] = program.toCharArray();
int unicodeCount = 0;
for (int i = 0; i < p.length; i++) {
if (p[i] > 127)
unicodeCount++;
}
if (unicodeCount == 0)
return program;
// if non-ascii chars are in there, convert to unicode escapes
// add unicodeCount * 5.. replacing each unicode char
// with six digit uXXXX sequence (xxxx is in hex)
// (except for nbsp chars which will be a replaced with a space)
int index = 0;
char p2[] = new char[p.length + unicodeCount * 5];
for (int i = 0; i < p.length; i++) {
if (p[i] < 128) {
p2[index++] = p[i];
} else if (p[i] == 160) { // unicode for non-breaking space
p2[index++] = ' ';
} else {
int c = p[i];
p2[index++] = '\\';
p2[index++] = 'u';
char str[] = Integer.toHexString(c).toCharArray();
// add leading zeros, so that the length is 4
//for (int i = 0; i < 4 - str.length; i++) p2[index++] = '0';
for (int m = 0; m < 4 - str.length; m++)
p2[index++] = '0';
System.arraycopy(str, 0, p2, index, str.length);
index += str.length;
}
}
return new String(p2, 0, index);
}
/**
* preprocesses a pde file and writes out a java file
* @return the class name of the exported Java
*/
private String write(final String program, final PrintWriter stream)
throws SketchException, RecognitionException, TokenStreamException {
// Match on the uncommented version, otherwise code inside comments used
// http://code.google.com/p/processing/issues/detail?id=1404
String uncomment = scrubComments(program);
PdeRecognizer parser = createParser(program);
Mode mode = parseMode(uncomment);
switch (mode) {
case JAVA:
try {
final PrintStream saved = System.err;
try {
// throw away stderr for this tentative parse
System.setErr(new PrintStream(new ByteArrayOutputStream()));
parser.javaProgram();
} finally {
System.setErr(saved);
}
setMode(Mode.JAVA);
} catch (Exception e) {
// I can't figure out any other way of resetting the parser.
parser = createParser(program);
parser.pdeProgram();
}
break;
case ACTIVE:
setMode(Mode.ACTIVE);
parser.activeProgram();
break;
case STATIC:
parser.pdeProgram();
break;
}
// set up the AST for traversal by PdeEmitter
//
ASTFactory factory = new ASTFactory();
AST parserAST = parser.getAST();
AST rootNode = factory.create(ROOT_ID, "AST ROOT");
rootNode.setFirstChild(parserAST);
makeSimpleMethodsPublic(rootNode);
// unclear if this actually works, but it's worth a shot
//
//((CommonAST)parserAST).setVerboseStringConversion(
// true, parser.getTokenNames());
// (made to use the static version because of jikes 1.22 warning)
BaseAST.setVerboseStringConversion(true, parser.getTokenNames());
final String className;
if (mode == Mode.JAVA) {
// if this is an advanced program, the classname is already defined.
className = getFirstClassName(parserAST);
} else {
className = this.name;
}
// if 'null' was passed in for the name, but this isn't
// a 'java' mode class, then there's a problem, so punt.
//
if (className == null)
return null;
// debug
if (false) {
final StringWriter buf = new StringWriter();
final PrintWriter bufout = new PrintWriter(buf);
writeDeclaration(bufout, className);
new PdeEmitter(this, bufout).print(rootNode);
writeFooter(bufout, className);
debugAST(rootNode, true);
System.err.println(buf.toString());
}
writeDeclaration(stream, className);
new PdeEmitter(this, stream).print(rootNode);
writeFooter(stream, className);
// if desired, serialize the parse tree to an XML file. can
// be viewed usefully with Mozilla or IE
if (Preferences.getBoolean("preproc.output_parse_tree")) {
writeParseTree("parseTree.xml", parserAST);
}
return className;
}
private PdeRecognizer createParser(final String program) {
// create a lexer with the stream reader, and tell it to handle
// hidden tokens (eg whitespace, comments) since we want to pass these
// through so that the line numbers when the compiler reports errors
// match those that will be highlighted in the PDE IDE
//
PdeLexer lexer = new PdeLexer(new StringReader(program));
lexer.setTokenObjectClass("antlr.CommonHiddenStreamToken");
// create the filter for hidden tokens and specify which tokens to
// hide and which to copy to the hidden text
//
filter = new TokenStreamCopyingHiddenTokenFilter(lexer);
filter.hide(PdePartialTokenTypes.SL_COMMENT);
filter.hide(PdePartialTokenTypes.ML_COMMENT);
filter.hide(PdePartialTokenTypes.WS);
filter.copy(PdePartialTokenTypes.SEMI);
filter.copy(PdePartialTokenTypes.LPAREN);
filter.copy(PdePartialTokenTypes.RPAREN);
filter.copy(PdePartialTokenTypes.LCURLY);
filter.copy(PdePartialTokenTypes.RCURLY);
filter.copy(PdePartialTokenTypes.COMMA);
filter.copy(PdePartialTokenTypes.RBRACK);
filter.copy(PdePartialTokenTypes.LBRACK);
filter.copy(PdePartialTokenTypes.COLON);
filter.copy(PdePartialTokenTypes.TRIPLE_DOT);
// Because the meanings of < and > are overloaded to support
// type arguments and type parameters, we have to treat them
// as copyable to hidden text (or else the following syntax,
// such as (); and what not gets lost under certain circumstances)
// -- jdf
filter.copy(PdePartialTokenTypes.LT);
filter.copy(PdePartialTokenTypes.GT);
filter.copy(PdePartialTokenTypes.SR);
filter.copy(PdePartialTokenTypes.BSR);
// create a parser and set what sort of AST should be generated
//
final PdeRecognizer parser = new PdeRecognizer(this, filter);
// use our extended AST class
//
parser.setASTNodeClass("antlr.ExtendedCommonASTWithHiddenTokens");
return parser;
}
/**
* Walk the tree looking for METHOD_DEFs. Any simple METHOD_DEF (one
* without TYPE_PARAMETERS) lacking an
* access specifier is given public access.
* @param node
*/
private void makeSimpleMethodsPublic(final AST node) {
if (node.getType() == PdeTokenTypes.METHOD_DEF) {
final AST mods = node.getFirstChild();
final AST oldFirstMod = mods.getFirstChild();
for (AST mod = oldFirstMod; mod != null; mod = mod.getNextSibling()) {
final int t = mod.getType();
if (t == PdeTokenTypes.LITERAL_private ||
t == PdeTokenTypes.LITERAL_protected ||
t == PdeTokenTypes.LITERAL_public) {
return;
}
}
if (mods.getNextSibling().getType() == PdeTokenTypes.TYPE_PARAMETERS) {
return;
}
final CommonHiddenStreamToken publicToken =
new CommonHiddenStreamToken(PdeTokenTypes.LITERAL_public, "public") {
{
setHiddenAfter(new CommonHiddenStreamToken(PdeTokenTypes.WS, " "));
}
};
final AST publicNode = new CommonASTWithHiddenTokens(publicToken);
publicNode.setNextSibling(oldFirstMod);
mods.setFirstChild(publicNode);
} else {
for (AST kid = node.getFirstChild(); kid != null; kid = kid
.getNextSibling())
makeSimpleMethodsPublic(kid);
}
}
protected void writeParseTree(String filename, AST ast) {
try {
PrintStream stream = new PrintStream(new FileOutputStream(filename));
stream.println("");
stream.println("");
OutputStreamWriter writer = new OutputStreamWriter(stream);
if (ast != null) {
((CommonAST) ast).xmlSerialize(writer);
}
writer.flush();
stream.println(" ");
writer.close();
} catch (IOException e) {
}
}
/**
*
* @param out
* @param programImports
* @param codeFolderImports
* @return the header offset
*/
protected int writeImports(final PrintWriter out,
final List programImports,
final List codeFolderImports) {
int count = writeImportList(out, getCoreImports());
count += writeImportList(out, programImports);
count += writeImportList(out, codeFolderImports);
count += writeImportList(out, getDefaultImports());
return count;
}
protected int writeImportList(PrintWriter out, List imports) {
return writeImportList(out, imports.toArray(new String[0]));
}
protected int writeImportList(PrintWriter out, String[] imports) {
int count = 0;
if (imports != null && imports.length != 0) {
for (String item : imports) {
out.println("import " + item + "; ");
count++;
}
out.println();
count++;
}
return count;
}
/**
* Write any required header material (eg imports, class decl stuff)
*
* @param out PrintStream to write it to.
* @param exporting Is this being exported from PDE?
* @param className Name of the class being created.
*/
protected void writeDeclaration(PrintWriter out, String className) {
if (mode == Mode.JAVA) {
// Print two blank lines so that the offset doesn't change
out.println();
out.println();
} else if (mode == Mode.ACTIVE) {
// Print an extra blank line so the offset is identical to the others
out.println("public class " + className + " extends PApplet {");
out.println();
} else if (mode == Mode.STATIC) {
out.println("public class " + className + " extends PApplet {");
out.println(indent + "public void setup() {");
}
}
/**
* Write any necessary closing text.
*
* @param out PrintStream to write it to.
*/
protected void writeFooter(PrintWriter out, String className) {
if (mode == Mode.STATIC) {
// close off setup() definition
out.println(indent + indent + "noLoop();");
out.println(indent + "}");
out.println();
}
if ((mode == Mode.STATIC) || (mode == Mode.ACTIVE)) {
// doesn't remove the original size() method, but calling size()
// again in setup() is harmless.
if (!hasMethod("settings") && sizeInfo.hasSettings()) {
out.println(indent + "public void settings() { " + sizeInfo.getSettings() + " }");
// out.println(indent + "public void settings() {");
// out.println(indent + indent + sizeStatement);
// out.println(indent + "}");
}
/*
if (sketchWidth != null && !hasMethod("sketchWidth")) {
// Only include if it's a number (a variable will be a problem)
if (PApplet.parseInt(sketchWidth, -1) != -1 || sketchWidth.equals("displayWidth")) {
out.println(indent + "public int sketchWidth() { return " + sketchWidth + "; }");
}
}
if (sketchHeight != null && !hasMethod("sketchHeight")) {
// Only include if it's a number
if (PApplet.parseInt(sketchHeight, -1) != -1 || sketchHeight.equals("displayHeight")) {
out.println(indent + "public int sketchHeight() { return " + sketchHeight + "; }");
}
}
if (sketchRenderer != null && !hasMethod("sketchRenderer")) {
// Only include if it's a known renderer (otherwise it might be a variable)
//if (PConstants.rendererList.hasValue(sketchRenderer)) {
out.println(indent + "public String sketchRenderer() { return " + sketchRenderer + "; }");
//}
}
if (sketchOutputPath != null && !hasMethod("sketchOutputPath")) {
out.println(indent + "public String sketchOutputPath() { return " + sketchOutputPath + "; }");
}
*/
if (!hasMethod("main")) {
out.println(indent + "static public void main(String[] passedArgs) {");
//out.print(indent + indent + "PApplet.main(new String[] { ");
out.print(indent + indent + "String[] appletArgs = new String[] { ");
if (Preferences.getBoolean("export.application.present")) {
out.print("\"" + PApplet.ARGS_PRESENT + "\", ");
String farbe = Preferences.get("run.present.bgcolor");
out.print("\"" + PApplet.ARGS_WINDOW_COLOR + "=" + farbe + "\", ");
if (Preferences.getBoolean("export.application.stop")) {
farbe = Preferences.get("run.present.stop.color");
out.print("\"" + PApplet.ARGS_STOP_COLOR + "=" + farbe + "\", ");
} else {
out.print("\"" + PApplet.ARGS_HIDE_STOP + "\", ");
}
// } else {
// // This is set initially based on the system control color, just
// // sets the color for what goes behind the sketch before it's added.
// String farbe = Preferences.get("run.window.bgcolor");
// out.print("\"" + PApplet.ARGS_BGCOLOR + "=" + farbe + "\", ");
}
out.println("\"" + className + "\" };");
out.println(indent + indent + "if (passedArgs != null) {");
out.println(indent + indent + " PApplet.main(concat(appletArgs, passedArgs));");
out.println(indent + indent + "} else {");
out.println(indent + indent + " PApplet.main(appletArgs);");
out.println(indent + indent + "}");
out.println(indent + "}");
}
// close off the class definition
out.println("}");
}
}
public String[] getCoreImports() {
return new String[] {
"processing.core.*",
"processing.data.*",
"processing.event.*",
"processing.opengl.*"
};
}
public String[] getDefaultImports() {
// These may change in-between (if the prefs panel adds this option)
//String prefsLine = Preferences.get("preproc.imports");
//return PApplet.splitTokens(prefsLine, ", ");
return new String[] {
"java.util.HashMap",
"java.util.ArrayList",
"java.io.File",
"java.io.BufferedReader",
"java.io.PrintWriter",
"java.io.InputStream",
"java.io.OutputStream",
"java.io.IOException"
};
}
/**
* Return true if this import should be removed from the code. This is used
* for packages like processing.xml which no longer exist.
* @param pkg something like processing.xml.XMLElement or processing.xml.*
* @return true if this shouldn't be added to the final code
*/
public boolean ignoreImport(String pkg) {
return false;
// return pkg.startsWith("processing.xml.");
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
/**
* Find the first CLASS_DEF node in the tree, and return the name of the
* class in question.
*
* TODO [dmose] right now, we're using a little hack to the grammar to get
* this info. In fact, we should be descending the AST passed in.
*/
String getFirstClassName(AST ast) {
String t = advClassName;
advClassName = "";
return t;
}
public void debugAST(final AST ast, final boolean includeHidden) {
System.err.println("------------------");
debugAST(ast, includeHidden, 0);
}
private void debugAST(final AST ast, final boolean includeHidden,
final int indent) {
for (int i = 0; i < indent; i++)
System.err.print(" ");
if (includeHidden) {
System.err.print(debugHiddenBefore(ast));
}
if (ast.getType() > 0 && !ast.getText().equals(TokenUtil.nameOf(ast))) {
System.err.print(TokenUtil.nameOf(ast) + "/");
}
System.err.print(ast.getText().replace("\n", "\\n"));
if (includeHidden) {
System.err.print(debugHiddenAfter(ast));
}
System.err.println();
for (AST kid = ast.getFirstChild(); kid != null; kid = kid.getNextSibling())
debugAST(kid, includeHidden, indent + 1);
}
private String debugHiddenAfter(AST ast) {
return (ast instanceof antlr.CommonASTWithHiddenTokens) ?
debugHiddenTokens(((antlr.CommonASTWithHiddenTokens) ast).getHiddenAfter()) : "";
}
private String debugHiddenBefore(AST ast) {
if (!(ast instanceof antlr.CommonASTWithHiddenTokens)) {
return "";
}
antlr.CommonHiddenStreamToken parent =
((antlr.CommonASTWithHiddenTokens) ast).getHiddenBefore();
if (parent == null) {
return "";
}
antlr.CommonHiddenStreamToken child = null;
do {
child = parent;
parent = child.getHiddenBefore();
} while (parent != null);
return debugHiddenTokens(child);
}
private String debugHiddenTokens(antlr.CommonHiddenStreamToken t) {
final StringBuilder sb = new StringBuilder();
for (; t != null; t = filter.getHiddenAfter(t)) {
if (sb.length() == 0) {
sb.append("[");
}
sb.append(t.getText().replace("\n", "\\n"));
}
if (sb.length() > 0) {
sb.append("]");
}
return sb.toString();
}
}