1    // Copyright (C) 2003 Adam Megacz <adam@xwt.org> all rights reserved.
2    //
3    // You may modify, copy, and redistribute this code under the terms of
4    // the GNU Library Public License version 2.1, with the exception of
5    // the portion of clause 6a after the semicolon (aka the "obnoxious
6    // relink clause")
7    
8    package org.xwt.util;
9    
10   import java.util.*;
11   import java.io.*;
12   
13   /**
14    *   A VERY crude, inefficient Java preprocessor
15    *
16    *   //#define FOO bar baz       -- replace all instances of token FOO with "bar baz"
17    *   //#replace foo/bar baz/bop  -- DUPLICATE everything between here and //#end,
18    *                                  replacing foo with bar and baz with bop in the *second* copy
19    *   //#switch(EXPR)             -- switch on strings
20    *       case "case1":
21    *   //#end
22    *
23    *   Replacements are done on a token basis.  Tokens are defined as a
24    *   sequence of characters which all belong to a single class.  The
25    *   two character classes are:
26    *
27    *     - [a-zA-Z0-9_]
28    *     - all other non-whitespace characters
29    */
30   public class Preprocessor {
31   
32       public static void main(String[] args) throws Exception {
33           BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
34           BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
35   
36           // process stdin to stdout
37           Preprocessor cc = new Preprocessor(br, bw);
38           Vector err = cc.process();
39           bw.flush();
40   
41           // handle errors
42           boolean errors = false;
43           for (int i=0; i < err.size(); i++) { if (err.get(i) instanceof Error) errors = true; System.err.println(err.get(i)); }
44           if (errors) throw new Exception();
45       }
46   
47       private Reader r;
48       private Writer w;
49       private LineNumberReader in;
50       private PrintWriter out;
51   
52       private Hashtable replace = new Hashtable();
53       private Hashtable repeatreplace = null;
54       private Vector sinceLastRepeat = null;
55       private Vector err = new Vector();
56   
57       private int enumSwitch = 0; // number appended to variable used in switch implementation
58   
59       public Preprocessor(Reader reader, Writer writer) {
60           setReader(reader);
61           setWriter(writer);
62       }
63   
64       public void setReader(Reader reader) { r = reader; if (r != null) in = new LineNumberReader(r); }
65       public Reader getReader() { return r; }
66   
67       public void setWriter(Writer writer) { w = writer; if (w != null) out = new PrintWriter(w); }
68       public Writer getWriter() { return w; }
69   
70   
71       /** process data from reader, write to writer, return vector of errors */
72       public Vector process() throws IOException {
73           err.clear();
74   
75           String s = null;
76   PROCESS:
77           while((s = in.readLine()) != null) {
78               if (sinceLastRepeat != null) sinceLastRepeat.addElement(s);
79               String trimmed = s.trim();
80   
81               if (trimmed.startsWith("//#define ")) {
82                   if (trimmed.length() == 9 || trimmed.charAt(9) != ' ') {
83                       err.add(new Error("#define badly formed, ignored")); continue PROCESS;
84                   }
85                   int keyStart = indexOfNotWS(trimmed, 9);
86                   if (keyStart == -1) {
87                       err.add(new Error("#define requires KEY")); continue PROCESS;
88                   }
89                   int keyEnd = indexOfWS(trimmed, keyStart);
90   
91                   int macroStart = trimmed.indexOf('(');
92                   int macroEnd = trimmed.indexOf(')');
93                   if (macroStart > keyEnd) {
94                       // no macro is defined, just make sure something dumb like KEYNA)ME hasn't been done
95                       if (macroEnd < keyEnd) { err.add(new Error("#define key contains invalid char: ')'")); continue PROCESS; }
96                       macroStart = macroEnd = -1;
97                   }
98   
99                   if (macroStart == 0) {
100                      err.add(new Error("#define macro requires name")); continue PROCESS;
101                  } else if (macroStart > 0) {
102                      if (macroStart > macroEnd) { err.add(new Error("#define macro badly formed")); continue PROCESS; }
103                      if (macroStart+1 == macroEnd) { err.add(new Error("#define macro requires property name")); continue PROCESS; }
104  
105                      JSFunctionMacro fm = new JSFunctionMacro();
106                      String key = trimmed.substring(keyStart, macroStart);
107                      String unbound = trimmed.substring(macroStart +1, macroEnd);
108                      int unboundDiv = unbound.indexOf(',');
109                      if (unboundDiv == -1) {
110                          fm.unbound1 = unbound;
111                      } else {
112                          fm.unbound1 = unbound.substring(0, unboundDiv);
113                          fm.unbound2 = unbound.substring(unboundDiv +1);
114                          if (fm.unbound1.length() == 0) { err.add(new Error("#define macro property 1 requires name")); continue PROCESS; }
115                          if (fm.unbound2.length() == 0) { err.add(new Error("#define macro property 1 requires name")); continue PROCESS; }
116                      }
117                      fm.expression = trimmed.substring(keyEnd).trim();
118                      replace.put(key, fm);
119                  } else {
120                      String key = trimmed.substring(keyStart, keyEnd);
121                      String val = trimmed.substring(keyEnd).trim();
122                      replace.put(key, val);
123                  }
124                  out.println(); // preserve line numbers
125                  
126              } else if (trimmed.startsWith("//#repeat ")) {
127                  trimmed = trimmed.substring(9);
128                  while(trimmed.charAt(trimmed.length() - 1) == '\\') {
129                      String s2 = in.readLine().trim();
130                      if (s2.startsWith("//")) s2 = s2.substring(2).trim();
131                      trimmed = trimmed.substring(0, trimmed.length() - 1) + " " + s2;
132                      out.println();  // preserve line numbers
133                  }
134                  StringTokenizer st = new StringTokenizer(trimmed, " ");
135                  repeatreplace = (Hashtable)replace.clone();
136                  while (st.hasMoreTokens()) {
137                      String tok = st.nextToken().trim();
138                      String key = tok.substring(0, tok.indexOf('/'));
139                      String val = tok.substring(tok.indexOf('/') + 1);
140                      repeatreplace.put(key, val);
141                  }
142                  sinceLastRepeat = new Vector();
143                  out.println(); // preserve line numbers
144  
145              } else if (trimmed.startsWith("//#end")) {
146                  if (sinceLastRepeat == null) { err.add(new Warning("#end orphaned")); continue PROCESS; }
147                  Hashtable save = replace;
148                  replace = repeatreplace;
149                  out.println();
150                  for(int i=0; i<sinceLastRepeat.size() - 1; i++) out.print(processLine((String)sinceLastRepeat.elementAt(i), true));
151                  sinceLastRepeat = null;
152                  replace = save;
153  
154              } else if (trimmed.startsWith("//#switch")) {
155                  int expStart = trimmed.indexOf('(') +1;
156                  if (expStart < 1) { err.add(new Error("expected ( in #switch")); continue PROCESS; }
157                  int expEnd = trimmed.lastIndexOf(')');
158                  if (expEnd == -1) { err.add(new Error("expected ) in #switch")); continue PROCESS; }
159                  if (expEnd - expStart <= 1) { err.add(new Error("badly formed #switch statement")); continue PROCESS; }
160                  String expr = trimmed.substring(expStart, expEnd);
161  
162                  out.println("final String ccSwitch"+enumSwitch+" = (String)("+expr+");");
163                  out.println("SUCCESS:do { switch(ccSwitch"+enumSwitch+".length()) {");
164  
165                  Hashtable[] byLength = new Hashtable[255];
166                  String key = null;
167                  String Default = null;
168                  for(trimmed = in.readLine().trim(); !trimmed.startsWith("//#end"); trimmed = in.readLine().trim()) {
169                      if (trimmed.startsWith("default:")) {
170                          Default = processLine(trimmed.substring(8), false);
171                          continue;
172                      }
173                      if (trimmed.startsWith("case ")) {
174                          // find key
175                          int strStart = trimmed.indexOf('\"') +1;
176                          if (strStart < 1) { err.add(new Error("expected opening of String literal")); continue PROCESS; }
177                          int strEnd = trimmed.indexOf('\"', strStart);
178                          if (strEnd == -1) { err.add(new Error("expected closing of String literal")); continue PROCESS; }
179                          key = trimmed.substring(strStart, strEnd);
180  
181                          Hashtable thisCase = (Hashtable)byLength[key.length()];
182                          if (thisCase == null) byLength[key.length()] = thisCase = new Hashtable();
183                          thisCase.put(key, "");
184  
185                          // find end of case definition
186                          int caseEnd = trimmed.indexOf(':', strEnd) +1;
187                          if (caseEnd < 1) { err.add(new Error("expected :")); continue PROCESS; }
188                          trimmed = trimmed.substring(caseEnd);
189                      }
190  
191                      if (key != null) {
192                          Hashtable hash = byLength[key.length()];
193                          hash.put(key, (String)hash.get(key) + processLine(trimmed, false) + "\n");
194                      } else {
195                          out.print(processLine(trimmed, false));
196                      }
197                  }
198  
199                  for(int i=0; i<255; i++) {
200                      if (byLength[i] == null) continue;
201                      out.println("case " + i + ": { switch(ccSwitch"+enumSwitch+".charAt(0)) {");
202                      buildTrie("", byLength[i]);
203                      out.println("}; break; }");
204                  }
205                  out.println("} //switch");
206                  if (Default != null) out.println(Default);
207                  out.println("} while(false); //OUTER");
208                  enumSwitch++;
209  
210              } else {
211                  out.print(processLine(s, false));
212              }
213          }
214  
215          return err;
216      }
217  
218      private void buildTrie(String prefix, Hashtable cases) {
219          Enumeration caseKeys = cases.keys();
220          Vec keys = new Vec();
221          while(caseKeys.hasMoreElements()) keys.addElement(caseKeys.nextElement());
222          keys.sort(new Vec.CompareFunc() { public int compare(Object a, Object b) {
223              return ((String)a).compareTo((String)b);
224          } } );
225  
226          for(int i=0; i<keys.size(); i++) {
227              if (!((String)keys.elementAt(i)).startsWith(prefix)) continue;
228              String prefixPlusOne = ((String)keys.elementAt(i)).substring(0, prefix.length() + 1);
229              if (i<keys.size()-1 && prefixPlusOne.equals((((String)keys.elementAt(i + 1)).substring(0, prefix.length() + 1)))) {
230                  out.println("case \'" + prefixPlusOne.charAt(prefixPlusOne.length() - 1) + "\': {");
231                  out.println("switch(ccSwitch"+enumSwitch+".charAt(" + (prefix.length()+1) + ")) {");
232                  buildTrie(prefixPlusOne, cases);
233                  out.println("} break; }");
234                  while(i<keys.size() && prefixPlusOne.equals(((String)keys.elementAt(i)).substring(0, prefix.length() + 1))) i++;
235                  if (i<keys.size()) { i--; continue; }
236              } else {
237                  out.println("case \'" + prefixPlusOne.charAt(prefixPlusOne.length() - 1) + "\':");
238                  String code = (String)cases.get(keys.elementAt(i));
239                  code = code.substring(0, code.length() - 1);
240                  String key = (String)keys.elementAt(i);
241                  out.println("if (\""+key+"\".equals(ccSwitch"+enumSwitch+")) { if (true) do { " + code + " } while(false); break SUCCESS; } break; ");
242              }
243          }
244      }
245  
246      private String processLine(String s, boolean deleteLineEndings) throws IOException {
247          if (deleteLineEndings && s.indexOf("//") != -1) s = s.substring(0, s.indexOf("//"));
248          String ret = "";
249          for(int i=0; i<s.length(); i++) {
250              char c = s.charAt(i);
251              if (!Character.isLetter(c) && !Character.isDigit(c) && c != '_') {
252                  ret += c;
253                  continue;
254              }
255              int j;
256              for(j = i; j < s.length(); j++) {
257                  c = s.charAt(j);
258                  if (!Character.isLetter(c) && !Character.isDigit(c) && c != '_') break;
259              }
260              String tok = s.substring(i, j);
261              Object val = replace.get(tok);
262              if (val == null) {
263                  ret += tok;
264                  i = j - 1;
265              } else if (val instanceof JSFunctionMacro) {
266                  if (s.charAt(j) != '(') { err.add(new Error("open paren must follow macro binding for macro " + tok)); continue; }
267                  ret += ((JSFunctionMacro)val).process(s.substring(j+1, s.indexOf(')', j)));
268                  i = s.indexOf(')', j);
269              } else {
270                  ret += val;
271                  i = j - 1;
272              }
273          }
274          if (!deleteLineEndings) ret += "\n";
275          return ret;
276      }
277  
278      private static int indexOfWS(String s) { return indexOfWS(s, 0); }
279      private static int indexOfWS(String s, int beginIndex) {
280          if (s == null || beginIndex >= s.length()) return -1;
281          for (; beginIndex < s.length(); beginIndex++) {
282              if (s.charAt(beginIndex) == ' ') return beginIndex;
283          }
284          return s.length();
285      }
286  
287      private static int indexOfNotWS(String s) { return indexOfWS(s, 0); }
288      private static int indexOfNotWS(String s, int beginIndex) {
289          if (s == null || beginIndex >= s.length()) return -1;
290          for (; beginIndex < s.length(); beginIndex++) {
291              if (s.charAt(beginIndex) != ' ') return beginIndex;
292          }
293          return -1;
294      }
295  
296      public class Warning {
297          protected String msg;
298          protected int line;
299  
300          public Warning() { msg = ""; }
301          public Warning(String m) { msg = m; if (in != null) line = in.getLineNumber(); }
302  
303          public String toString() { return "WARNING Line "+line+": "+msg; }
304      }
305  
306      public class Error extends Warning {
307          public Error() { super(); }
308          public Error(String m) { super(m); }
309          public String toString() { return "ERROR Line "+line+": "+msg; }
310      }
311  
312      public static class JSFunctionMacro {
313          public String unbound1 = null;
314          public String unbound2 = null;
315          public String expression = null;
316          public String process(String args) {
317              String bound1 = null;
318              String bound2 = null;
319              if (unbound2 == null) {
320                  bound1 = args;
321                  return expression.replaceAll(unbound1, bound1);
322              } else {
323                  bound1 = args.substring(0, args.indexOf(','));
324                  bound2 = args.substring(args.indexOf(',') + 1);
325                  return (expression.replaceAll(unbound1, bound1).replaceAll(unbound2, bound2));
326              }
327          }
328      }
329  }
330  
331