/* * OddTruth.java * * Copyright (c) 2007 Shawn Vincent. All Rights Reserved. * Hereby donated to the public domain. */ package com.svincent.util; import java.io.*; import com.svincent.util.Cloptus.*; /** * A simple and powerful Java template mechanism. * * This is a simple Java source preprocessor that allows pleasant and * convenient generation of HTML (and other textual output). * * OddTruth defines these simple rules: * * 1. Java code goes right through: the file starts in a Java context. * * This means an OddTruth program is a Java program: you can do * anything you want. * * 2. Hash (#) at the beginning of a line denotes template text: * it is wrapped in 'out.print (...)' * * This is an unobtrusive syntax for outputting HTML and the like. * * 3. In template text, '{...}' is a Java expression: it is wrapped in * 'out.oddPrint(...)' * * You often want to embed the results of expressions. * * 4. In template text, '{.ID(...,...)}' is a method call on 'out': it * becomes 'out.ID(...,...)' * * This lets you do different escaping mechanisms in a clean, * powerful way. * * 5. In template text, Java backslash escape sequences are allowed. * * Often you want to output weird characters (i.e. high Unicode * characters) - this lets you do it using a syntax you're * familiar with, along with providing a natural means to escape * magic characters (like '{'). * * 6. Hash-quote (#") at the beginning of a line generates a String, * instead of printing to out. Strings on subsequent lines are * automatically concatenated. * * Sometimes it's convenient to store the results of a template * in a String -- this provides a natural mechanism for doing * just that. * * An additional rule might be nice: Some kind of multi-line context * (maybe ### ... ###) -- kindof a pain to add right now -- maybe some * day. * * XXX feature to add: better locus information for OddTruth error messages. * * Example program: * * class MyHomePage { * public void viewPage (HtmlWriter out) * { * # * #
* #Hi: it's currently {new Date()}, and I like you lots.
* # * # * # * # * } * public String getEmailAddress () { ... } * } * * Also see RunOddTruth: an integration of OddTruth with BeanShell so * that you can run OddTruth programs directly! **/ public class OddTruth { File[] inputFiles; /** * **/ public static void main (String[] args) { try { new OddTruth ().run (args); } catch (IOException ex) { ex.printStackTrace (); } } /** * **/ public void run (String[] args) throws IOException { // --- process arguments. OptSet opts = new OptSet (); FileOpt inOpt = new FileOpt (opts, "in") .description ("List of files to process") .positional (true) .list (true) .required (true); ParsedArgs parsedArgs = opts.run (args); if (parsedArgs == null) return; inputFiles = inOpt.getArray (parsedArgs); System.out.println ("Odd Truth compiling " +inputFiles.length +" source file" +(inputFiles.length == 1 ? "" : "s")); preprocessJavaFiles (); } /** * **/ public void preprocessJavaFiles () throws IOException { for (File f : inputFiles) preprocessJavaFile (f); } /** * **/ public static void preprocessJavaFile (File f) throws IOException { String path = f.getPath (); // XXX hack. Don't process Emacs temp files if (path.endsWith ("~") || path.endsWith ("#")) return; int extensionIndex = path.lastIndexOf ('.'); String strippedExtension; if (extensionIndex == -1) strippedExtension = path; else strippedExtension = path.substring (0, extensionIndex); String newPath = strippedExtension + ".java"; Reader in = new FileReader (f); PrintWriter out = new PrintWriter (new FileWriter (new File (newPath)), true); preprocessJavaFile (in, out); } // /** // * // **/ // public static String preprocessJavaFile (String file) // { // String[] lines = file.split ("\n"); // StringBuilder out = new StringBuilder (); // for (String line:lines) // { // out.append (preprocessJavaLine (line)); // out.append ("\n"); // } // return out.toString (); // } /** * **/ public static void preprocessJavaFile (Reader _in, PrintWriter out) throws IOException { BufferedReader in = new BufferedReader (_in); boolean prevLineWasTemplate = false; String line = in.readLine (); while (line != null) { // --- only lines starting with '#' are touched. if (line.trim ().startsWith ("#")) { out.println (preprocessTemplateTextLine(line,prevLineWasTemplate)); prevLineWasTemplate = true; } // --- other lines are just Java: no preprocessing. else { out.println (line); prevLineWasTemplate = false; } line = in.readLine (); } out.flush (); } /** * **/ protected static String preprocessTemplateTextLine (String line, boolean prevLineWasTemplate) { StringBuilder out = new StringBuilder (); int grapesIndex = line.indexOf ('#'); if (grapesIndex == -1) throw new OddTruthException ("Template text line must start with '#' character."); int pos = grapesIndex+1; boolean outputString = line.length () > pos && line.charAt (pos) == '"'; if (outputString) pos++; // skip open quote. // --- output preceeding whitespace (so resulting Java code looks nicer) out.append (line.substring (0, grapesIndex)); if (outputString) { // --- concatenate subsequent string lines together. if (prevLineWasTemplate) out.append ('+'); out.append ('"'); } else out.append ("{"); int squiggleIndex = findNextOpenSquiggle (line, pos); while (squiggleIndex != -1) { // --- if the squiggle doesn't start the next section, we've // --- got template text. Output it. if (squiggleIndex > pos) { String templateText = line.substring (pos, squiggleIndex); // --- parse Java string escapes. templateText = unquoteJavaStringLiteral (templateText); if (outputString) out.append (quoteJavaStringLiteral (templateText)); else out.append ("out.print (\"" +quoteJavaStringLiteral (templateText) +"\"); "); } // --- now, 'squiggleIndex' is at the start of the Java // --- expression. Find the end. int endSquiggleIndex = findClosingSquiggle (line, squiggleIndex); String embeddedJava = line.substring (squiggleIndex, endSquiggleIndex+1); out.append (processEmbeddedJava (embeddedJava, outputString)); pos = endSquiggleIndex+1; squiggleIndex = findNextOpenSquiggle (line, pos); } if (pos < line.length ()) { // --- Extract the template text at the end of the line. String templateText = line.substring (pos); // --- parse Java string escapes. templateText = unquoteJavaStringLiteral (templateText); if (outputString) out.append (quoteJavaStringLiteral (templateText)+"\\n"); else out.append ("out.println (\"" +quoteJavaStringLiteral (templateText) +"\"); "); } else { if (outputString) out.append ("\\n"); else out.append ("out.println ();"); } if (outputString) out.append ('"'); else out.append ("}"); return out.toString (); } protected static int findNextOpenSquiggle (String line, int pos) { int openSquiggle = line.indexOf ('{', pos); // --- skip escaped squiggles. while (openSquiggle != -1 && openSquiggle > 0 && line.charAt (openSquiggle-1) == '\\') openSquiggle = line.indexOf ('{', openSquiggle+1); return openSquiggle; } /** * **/ protected static String processEmbeddedJava (String embeddedJava, boolean outputString) { if (!embeddedJava.startsWith ("{") || !embeddedJava.endsWith ("}")) throw new OddTruthException ("Embedded Java must be surrounded by squiggles."); embeddedJava = embeddedJava.substring (1, embeddedJava.length () - 1); if (outputString) return embeddedJava; else if (embeddedJava.startsWith (".")) return "out"+embeddedJava+"; "; // XXX error checking here? else return "out.oddPrint ("+embeddedJava+"); "; //XXX error checking here? } /** * **/ protected static int findClosingSquiggle (String line, int openingSquiggle) { boolean inStringLiteral = false; boolean inCharLiteral = false; for (int i=openingSquiggle+1; i* * Quoting is performed first as per JLS, section 3.10.6: "Escape * Sequences for Character and String Literals". This quotes most * of the normal characters such as \b and \n, etc.
* * Secondly, any non-printable-ASCII character (including the * control values in low ASCII, as well as any non-ASCII Unicode * character) is quoted using Unicode escapes, as per JLS, section * 3.3: "Unicode Escapes".
**/
public static String lookupQuotedForm (char c, boolean stringConstant)
{
switch (c)
{
case '\b': return "\\b";
case '\t': return "\\t";
case '\n': return "\\n";
case '\f': return "\\f";
case '\r': return "\\r";
case '"':
if (stringConstant) return "\\\"";
else return null;
case '\'':
if (!stringConstant) return "\\'";
else return null;
case '\\': return "\\\\";
default:
if (!(c >= ' ' && c <= '~'))
{
return
"\\u"
+ to4DigitHex (c);
}
else
return null;
}
}
public static String to4DigitHex (int n)
{
String hexString = Integer.toHexString (n);
return ("0000".substring (0, 4 - hexString.length ())) + hexString;
}
public static String quoteHtml (String html)
{
if (html == null) return null;
StringBuffer out = new StringBuffer ();
int len = html.length ();
for (int i=0; i