Chapter 12: Crafting Your Own Template Language

Summary

In this chapter we designed a new template language. It was designed to be concise and simple, using XPath for tree navigation and the Java language for expressions. The language has two forms: a simple textual easy-to-read language and an XML form. A program easily converts between the two, thus allowing simple textual tools and XML tools. Whitespace is managed through nonobtrusive controls. The original template language examples designed in this chapter are available below. The up-to-date open source version is available at http://craigc.com/pg/tl.



Chapter 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13

Example 12-26: PlayPG.template (Simple Form)
Example 12-27: TL2Java.template (Except generate2 Method-Example 12-28)
Example 12-28: The generate2 Method

Example 12-26: PlayPG.template (Simple Form)

import java.awt.*;
import java.awt.event.*;

# String name = context.evalString("//play/@name"); #
class #(name)#Play2 extends Frame {
    /* The Props for #(name)# ************/
#for "//play/prop"#
    Button #"@name"#Prop 
              = new Button("#"trait"#");
#end#

    /* The Events in the #(name)# ********/

    class PropEvent implements ActionListener {
        public void actionPerformed(ActionEvent evt) {
            Object prop = evt.getSource();
#for "//play/prop"#
  # if "position()!=1"#
	      } else
  #fi# if (prop.equals(#"@name"#Prop)) {
    #for "trait"#
                #(getPropName(context))#Prop.setLabel("#"."#")
    #end#
  #if "./script/@goto"#
                enterNewScene("#"script/@goto"#");
  #fi#
#end#
            } else {
                System.out.println("Invalid prop");
            }
        }
    }

    /* Creating and starting up the #(name)# ****/

    String currentScene;

    public #(name)#Play2() {
# String title = context.evalString("//play/title");
  if (title.equals("")) title = "No Title";
#
        super("#(title)#");
        setSize(#"//play/@width"#, #"//play/@height"#);
        setLayout(new FlowLayout());

        // initialize props
        PropEvent a = new PropEvent();
#for "//play/prop"#
        #"@name"#Prop.addActionListener(a);
#end#
        // start scene
        enterNewScene("#"//play/@start"#");
    }

    public void enterNewScene(String scene) {
        removeAll();  // remove previous scene
        currentScene = scene;
#for "//play/scene"#
  #if "position()!=1"#
	  } else
  #fi# if (scene.equals("#"@name"#")) {
  #for a "addprop"#
                add(#"@name"#Prop);
  #end#
	          setBackground(Color.decode("#"@color"#"));
#end#
        } else {
            System.out.println("Invalid scene: "+scene);
        }
        show();
    }

    public static void main(String[] args) {
        new #(name)#Play2();
    }
}

#declarations
/** gets appropriate prop name for a trait */
String getPropName(XPathContext c) throws Exception {
    String n = c.evalString("@prop");
    if (!n.equals("")) {
        return n;
    }
    return c.evalString(
          "ancestor::addprop/@name | ancestor::prop/@name");
}
#

Example 12-27: TL2Java.template (Except generate2 Method-Example 12-28)

<?xml version="1.0"?>
<tl>
package com.craigc.progen;

import org.w3c.dom.*;
import java.io.*;
import java.util.*;
<for path="//declare/@import">
import <value path="."/>;
</for>

<java>
String className = getClassName(context.evalString("//declare/@classname"));
</java>

public class <expr>className</expr> {
    Properties properties = new Properties();

<for path="//declarations">
<value path="."/>
</for>

    public boolean generate(XPathContext context, ProgramWriter out) {
        try {
<java>generate2(context, out); </java>
        } catch (Exception e) {
            System.out.println("Exception: "+e.getMessage());
            e.printStackTrace();
            return false;
        }
        return true;
    }
        
    public static void main(String[] args) {
        try {
            ProgramWriter out = args.length>=2
                ?new ProgramWriter(new FileOutputStream(args[1]))
                :new ProgramWriter(System.out);
            <expr>className</expr> pg = new <expr>className</expr>();
            for (int j=1; jlt;=args.length; ++j) {
                pg.properties.put("arg"+j, args[j-1]);
            }
            pg.generate(new XPathContext(args[0]), out);
            out.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    String[][] history = {
        { "<expr>new Date()</expr>", // date this file was generated
             <expr>quoteString(properties.getProperty("arg1"))</expr>,
             <expr>quoteString(properties.getProperty("arg2"))</expr> },
<java whitespace="reverse">
            for (int j=0; j&lt;history.length; ++j) {
                out.print("        ");
                for (int k=0; k&lt;history[j].length; ++k) {
                    out.print(quoteString(history[j][k])+", ");
                }
                out.println("}, ");
            }
</java>
    };

}

<declarations>
String ind = "    ";
int uid = 0;

public boolean generate2(XPathContext context, ProgramWriter out) {
	// See Example 12-28
}

public String quoteString(String t) {
     // add quotes and convert new lines, tabs  quotes
}

public String strip(String t) {
    // remove trailing white space characters
}

public String getClassName(String className) {
    // if className is an empty string, get class name from properties.get("arg2")
}
</declarations>

</tl>

Example 12-28: The generate2 Method

public boolean generate2(XPathContext context, ProgramWriter out) {
    out.setIndent(out.getIndent()+ind);
    out.print(ind);
    try {
        NodeList x = context.evalNodeSet("*|text()");
        for (int j=0; j<x.getLength(); ++j) {
            Node n = (Node) x.item(j);
            boolean strip = true;
            if (j<x.getLength()-1) {
                Node n2 = (Node) x.item(j+1);
                int t2 = n2.getNodeType();
                if (t2==Node.TEXT_NODE) {
                    strip = false;
                } else if (t2==Node.ELEMENT_NODE) {
                    String ename = n2.getNodeName();
                    if (ename.equals("value") || ename.equals("expr")) {
                        strip = false;
                    }
                    context.push(n2, 1, 1);
                    String reverse = context.evalString("@whitespace");
                    if (reverse.equals("reverse")) {
                        strip = !strip;
                    }
                }
            }
            context.push(n, j+1, x.getLength());
            switch (n.getNodeType()) {
            case Node.ELEMENT_NODE:
                String ename = n.getNodeName();
                if (ename.equals("value")) {
                    out.println("out.print(context.evalString(\""
                        +context.evalString("@path")+"\"));");
                } else if (ename.equals("for")) {
                    ++uid;
                    String var = context.evalString("@var");
                    if (var.equals("")) {
                        var = "loopvar"+uid;
                    }
                    out.println("NodeList x"+uid
                        +" = context.evalNodeSet(\""
                        +context.evalString("@path")+"\");");
                    out.println("for (int j"+uid+"=0; j"+uid+"lt;x"+uid
                        +".getLength(); ++j"+uid+") {");
                    out.println(ind+"Node "+var+" = (Node) x"+uid
                        +".item(j"+uid+");");
                    out.println(ind+"context.push("+var+", j"+uid
                        +"+1, x"+uid+".getLength());");
                    generate2(context, out);
                    out.println("context.pop();");
                    out.println("}");
                } else if (ename.equals("if")) {
                    out.println("if (context.evalBoolean(" 
                      +quoteString(context.evalString("@path"))+")) {");
                    generate2(context, out);
                    out.println("\n}");
                } else if (ename.equals("else")) {
                    out.setIndent(out.getIndent()+ind);
                    out.println("} else {");
                    generate2(context, out);
                    out.setIndent(out.getIndent().substring(
                                           ind.length()));
                } else if (ename.equals("expr")) {
                    out.println("out.print("+
                        context.evalString(".")+");");
                } else if (ename.equals("java")) {
                    out.println(context.evalString("."));
                } else if (ename.equals("empty")) {
                } else if (ename.equals("declare")) {
                } else if (ename.equals("declarations")) {
                } else {
                    out.println("//Unknown Element: "+n.getNodeName());
                }
                break;
            case Node.TEXT_NODE:
                String t = context.evalString("."); // n.toString();
                if (strip) {
                    t = strip(t);
                }
                if (!t.equals("")) {
                    out.println("out.print("+quoteString(t)+");");
                }
                break;
            }
            context.pop();
        }
    } catch (Exception e) {
        System.out.println("Exception in generate2: "+e.getMessage());
        e.printStackTrace();
        return false;
    } finally {
        out.setIndent(out.getIndent().substring(ind.length()));
    }
    return true;
}