001// Copyright (c) 2001 Hursh Jain (http://www.mollypages.org) 
002// The Molly framework is freely distributable under the terms of an
003// MIT-style license. For details, see the molly pages web site at:
004// http://www.mollypages.org/. Use, modify, have fun !
005
006package fc.util.pagetemplate;
007
008import java.io.*;
009import java.util.*;
010import java.util.regex.*;
011import fc.io.*;
012import fc.util.*;
013
014/*
015NOTES
016
017Code blocks of the form 
018 [...] 
019cause problems with java arrays
020
021String[] or foo[4] etc.., craps out. So we need to use 
022 [[...]] 
023for the molly code blocks
024
025The order of switch'es in a case statement in various methods is not always arbitrary
026(the order matters in this sort of recursive descent parsing)
027
028Read www.mollypages.org/page/grammar/index.mp for a intro to parsing
029*/
030
031/**
032Parses a template page and writes out the corresponding java file to the specified
033output. The parser and scanner is combined into one class here for simplicity (a
034seperate scanner is overkill for a simple LL(1) grammar such as page templates).
035
036@author hursh jain
037*/
038public final class TemplateParser
039{
040private static final boolean dbg    = false;
041private static final int     EOF    = -1;
042private              int     dbgtab = 0;
043
044String           classname;
045String         packagename = TemplatePage.PACKAGE_NAME;
046TemplateReader   in;
047PrintWriter      out;
048Log              log;
049File             inputFile;
050File             outputFile;
051boolean          includeMode = false;
052
053//Read data
054//we use these since stringbuffer/builders do not have a clear/reset function
055CharArrayWriter buf = new CharArrayWriter(4096);
056CharArrayWriter wsbuf = new CharArrayWriter(32);  // ^(whitespace)* 
057int c = EOF;
058
059//PageData
060List  decl       = new ArrayList();     //declarations
061List  inc_decl       = new ArrayList();     //external included declarations
062List  imps           = new ArrayList();     //imports
063List  tree           = new ArrayList();     //code, exp, text etc.
064Map   directives     = new HashMap();       //page options
065Set   circularityTrack = new HashSet();   //track pages already included to stop circular refs
066
067/** The name of the  ("remove-initial-whitespace") directive */
068public static String d_remove_initial_emptylines = "remove-initial-whitespace";
069/** The name of the  ("remove-all-emptylines") directive */
070public static String d_remove_all_emptylines = "remove-all-emptylines";
071
072/* 
073This constructor for internal use.
074
075The parser can be invoked recursively to parse included files as
076well..that's what the includeMode() does (and this construtor is invoked
077when including). When including, we already have a output writer
078created, we use that writer (instead of creating a new one based on
079src_encoding as we do for in normal page parsing mode).
080
081@param  contextRoot absolute path to the webapp context root directory
082@param  input   absolute path to the input page file
083@param  input   absolute path to the output file (to be written to).
084@param  classname classname to give to the generated java class.
085*/
086private TemplateParser(File input, PrintWriter outputWriter, String classname, Log log) 
087throws IOException
088  {
089  this.inputFile = input; 
090  this.in  = new TemplateReader(input);
091  this.out = outputWriter;
092  this.classname = classname;
093  this.log = log;
094
095  circularityTrack.add(input.getAbsolutePath());
096  }
097
098
099/**
100Creates a new page parser.
101
102@param  contextRoot absolute path to the webapp context root directory
103@param  input   absolute path to the input page file
104@param  output    absolute path to the output file (to be written to).
105@param  classname classname to give to the generated java class.
106@log  log     destination for internal logging output.
107*/
108public TemplateParser(File input, File output, String classname, Log log) 
109throws IOException
110  {
111  this.inputFile = input; 
112  this.in  = new TemplateReader(input);
113  this.outputFile = output;
114  this.classname = classname;
115  this.log = log;
116
117  circularityTrack.add(input.getAbsolutePath());
118  }
119
120void append(final int c)
121  {
122  Argcheck.istrue(c >= 0, "Internal error: recieved c=" + c);
123  buf.append((char)c);
124  }
125
126void append(final char c)
127  {
128  buf.append(c);
129  }
130
131void append(final String str)
132  {
133  buf.append(str);
134  }
135
136/* not used anymore */
137TemplateParser includeMode()
138  {
139  includeMode = true;
140  return this;
141  }
142
143/**
144Parses the page. If the parse is successful, the java source will be
145generated.
146
147@throws IOException   a parse failure occurred. The java source file
148            may or may not be properly generated or written
149            in this case.
150*/
151public void parse() throws IOException
152  {
153  parseText();  
154  if (! includeMode)  {
155    writePage();
156    out.close();
157    }
158  else{
159    out.flush();
160    }
161  in.close();
162  }
163
164//util method for use in the case '[' branch of parseText below.
165private Text newTextNode()
166  {
167  Text text = new Text(buf);
168  tree.add(text);
169  buf.reset();
170  return text;
171  }
172  
173void parseText() throws IOException
174  {
175  if (dbg) dbgenter(); 
176      
177  while (true)
178    { 
179    c = in.read();
180    
181    if (c == EOF) {
182      tree.add(new Text(buf));
183      buf.reset();
184      break;
185      }
186      
187    switch (c)
188      { 
189      //Escape start tags
190      case '\\':
191        /*  we don't need to do this: previously, expressions
192        were [...] but now they are [=...], previously we needed
193        to escape \[[ entirely (since if we escaped \[ the second
194        [ would start an expression
195        */
196        /*        
197        if (in.match("[["))  
198          append("[[");
199        */
200        //escape only \[... otherwise leave \ alone
201        if (in.match("["))
202          append("[");
203        else
204          append(c);
205        break;
206
207      case '[':
208        /* suppose we have
209        \[[
210        escape handling above will capture \[
211        then the second '[' drops down here. Good so far.
212        But we must not create a new text object here by
213        default...only if we see another [[ or [= or [include or
214        whatever. 
215        */
216        /*
217        But creating a text object at the top is easier
218        then repeating this code at every if..else branch below
219        but this creates superfluous line breaks.
220        
221        hello[haha]world
222        -->prints as-->
223        hello  (text node 1)
224        [haha] (text node 2)
225        world  (text node 3)
226        --> we want
227        hello[haha]world (text node 1)
228        */
229          
230        if (in.match('[')) { 
231          newTextNode();
232          parseCode(); 
233          }
234        else if (in.match('=')) {
235          Text text = newTextNode();
236          parseExpression(text);
237          }
238        else if (in.match('!')) {
239          newTextNode();
240          parseDeclaration();
241          }
242        else if (in.match("/*")) {
243          newTextNode();
244          parseComment(); 
245          }
246        else if (in.matchIgnoreCase("page")) {
247          newTextNode();
248          parseDirective();
249          }
250        else if (in.matchIgnoreCase("include-file")) {
251          newTextNode();
252          parseIncludeFile();
253          }
254        else if (in.matchIgnoreCase("include-decl")) {
255          newTextNode();
256          parseIncludeDecl();
257          }
258        else if (in.matchIgnoreCase("import")) {
259          newTextNode();
260          parseImport();
261          }
262        else  {
263          //System.out.println("c1=" + (char)c);
264          append(c);
265          }
266        break;  
267  
268      default:
269        //System.out.println("c2=" + (char)c);
270        append(c);
271        
272      } //switch    
273    } //while
274    
275  if (dbg) dbgexit(); 
276  }
277  
278void parseCode() throws IOException
279  {
280  if (dbg) dbgenter(); 
281
282  int startline = in.getLine();
283  int startcol = in.getCol();
284  
285  while (true)
286    {
287    c = in.read();  
288  
289    switch (c) /* the order of case tags is important. */
290      {
291      case EOF:
292        unclosed("code", startline, startcol);
293        if (dbg) dbgexit(); 
294        return;
295
296      case '/':   //Top level:  // and /* comments
297        append(c);
298        c = in.read();
299        append(c);
300        if (c == '/') 
301          appendCodeSlashComment();
302        else if (c == '*') 
303          appendCodeStarComment();
304          break;        
305    
306      case '"':     //strings outside of any comment
307        append(c);
308        appendCodeString();  
309        break;
310        
311      case '\'':
312        append(c);
313        appendCodeCharLiteral();
314        break;
315        
316      case ']':
317        if (in.match(']')) {
318          tree.add(new Code(buf));
319          buf.reset();
320          if (dbg) dbgexit(); 
321          return;
322          }
323        else {
324          append(c);
325          }
326        break;
327      
328      /* 
329      a hash by itself on a line starts a hash section.
330      whitespace before the # on that line is used as an
331      printing 'out' statements for that hash.
332      
333      for (int n = 0; n < ; n++) {
334      ....# foo #
335      | }
336      |=> 4 spaces 
337      so nice if generated code looked like:
338      
339      for (int n = 0; n < ; n++) {
340          out.print(" foo ");
341          }
342      */
343      case '\n':
344      case '\r':
345        append(c);       //the \n or \r just read
346        readToFirstNonWS();  //won't read past more newlines 
347        //is '#' is first non-ws on this line ?
348        c = in.read();
349        if (c == '#') {           
350          tree.add(new Code(buf));
351          buf.reset();
352          //whitespace provides indentation offset
353          parseHash(wsbuf.toString()); 
354          }
355        else{
356          append(wsbuf.toString());  //wsbuf contains codetext
357          //let other cases also handle first non-ws or EOF
358          in.unread();    
359          }
360        break;
361      
362      /* in this case, hash does not start on a new line, like:
363         for (...) { #
364      */
365      case '#':
366        tree.add(new Code(buf));
367        buf.reset();
368        parseHash(null);
369        break;  
370      
371      default:
372        append(c);
373      } //switch    
374    } //while
375  }
376
377void parseHash(String offset) throws IOException
378  {
379  if (dbg) dbgenter(); 
380
381  int startline = in.getLine();
382  int startcol = in.getCol();
383
384  while (true)
385    {
386    c = in.read();  
387  
388    switch (c)
389      {
390      case EOF: 
391        unclosed("hash", startline, startcol);
392        if (dbg) dbgexit(); 
393        return;
394
395      //special case: very common and would be a drag to escape
396      //this every time:
397      //  # <table bgcolor="#ffffff">....   #
398      //Now, all of:
399      //  bgcolor="#xxx"  
400      //  bgcolor='#xxx'
401      //  bgcolor="\#xxx" 
402      //will work the same and give: bgcolor="#xxx"
403      //1)
404      //However to get a:
405      //  bgcolor=#xxx    (no quoted around #xxx)
406      //we still have to say:
407      //  bgcolor=\#xxx   
408      //2)
409      //Of course, since we special case this, then:
410      //  #"bar"#
411      // that ending # is lost and we end up with
412      //  #"bar"  with no closing hash
413      //So we need to make sure that we write:
414      //  #"bar" #
415      // instead
416
417      case '\'':
418      case '"':
419        append(c);
420        if (in.match('#')) 
421          append('#');
422        break;
423        
424      case '\\':
425        if (in.match('[')) 
426          append('[');      
427        else if (in.match('#'))
428          append('#');
429        else
430          append(c);
431        break;
432        
433      case '[':
434        if (in.match('=')) {
435          Hash hash = new Hash(offset, buf);
436          tree.add(hash);
437          buf.reset();
438          parseExpression(hash);
439          }
440        else{
441          append(c);
442          }
443        break;
444
445      /*
446      this case is not needed but is a bit of a optimization
447      for (int n = 0; n < 1; n++) {
448        #
449        foo
450      ....#...NL
451        }
452      avoids printing the dots (spaces) and NL in this case
453      (the newline after foo is still printed)
454      */
455      case '\n':
456      case '\r':
457        append(c);
458        readToFirstNonWS(); 
459        c = in.read();
460        //'#' is first non-ws on the line
461        if (c == '#') {
462          tree.add(new Hash(offset, buf));
463          buf.reset();
464          //skipIfWhitespaceToEnd();
465          if (dbg) dbgexit(); 
466          return;
467          }
468        else {
469          append(wsbuf.toString());
470          in.unread(); //let other cases also handle first non-ws   
471          }
472        break;
473
474      case '#':
475        tree.add(new Hash(offset, buf));  
476        //skipIfWhitespaceToEnd();
477        buf.reset();
478        if (dbg) dbgexit(); 
479        return;
480        
481      default:
482        append(c);
483      }  //switch 
484    } //while
485  }
486
487/**
488[page <<<FOO]
489...as-is..no parse, no interpolation..
490FOO
491*/
492void parseHeredoc(StringBuilder directives_buf) throws IOException
493  {
494  if (dbg) dbgenter(); 
495
496  int startline = in.getLine();
497  int startcol = in.getCol();
498      
499  int i = directives_buf.indexOf("<<<"); /* "<<<".length = 3 */
500  CharSequence subseq = directives_buf.substring(
501            i+3, 
502            /*directives_buf does not have a ending ']' */
503            directives_buf.length() 
504            );
505    
506  final String      heredoc     = subseq.toString().trim();
507  final int         heredoc_len = heredoc.length();
508  final CharArrayWriter heredoc_buf = new CharArrayWriter(2048);
509
510  /* 
511  the ending heredoc after newline speeds things up a bit
512  which is why is traditionally used i guess, otherwise
513  we have to try a full match every first match. this 
514  implementation doesn't care where the ending heredoc
515  appears (can be anywhere)...simplifies the implementation.
516  */
517  
518  while (true)
519    { 
520    c = in.read();
521    
522    if (c == EOF) {
523      unclosed("heredoc: <<<"+heredoc, startline, startcol);
524      break;
525      }
526      
527    if (c == heredoc.charAt(0))
528      {
529      boolean matched = true;
530      if (heredoc_len > 1) {
531        matched = in.match(heredoc.substring(1));
532        }
533      if (matched) {  
534        tree.add(new Heredoc(heredoc_buf));
535        break;
536        }
537      }
538    
539    //default action
540    heredoc_buf.append((char)c);  
541    } //while
542    
543  if (dbg) dbgexit(); 
544  }
545
546/*
547Text is the parent node for the expression. A new expression is parsed,
548created and added to the text object by this method
549*/
550void parseExpression(Element parent) throws IOException
551  {
552  if (dbg) dbgenter(); 
553
554  int startline = in.getLine();
555  int startcol = in.getCol();
556
557  while (true)
558    {
559    c = in.read();      
560  
561    switch (c)
562      {
563      case EOF:
564        unclosed("expression", startline, startcol);
565        if (dbg) dbgexit(); 
566        return;
567
568      case '\\':
569        if (in.match(']')) 
570          append(']');    
571        else
572          append(c);
573        break;
574
575      case ']':
576        if (buf.toString().trim().length() == 0)
577          error("Empty expression not allowed", startline, startcol);
578        parent.addExp(new Exp(buf));
579        buf.reset();  
580        if (dbg) dbgexit(); 
581        return;
582        
583      default:
584        append(c);
585      }
586    }
587  }
588
589void parseComment() throws IOException
590  {
591  if (dbg) dbgenter(); 
592
593  int startline = in.getLine();
594  int startcol = in.getCol();
595
596  while (true)
597    {
598    c = in.read();      
599  
600    switch (c)
601      {
602      case EOF:
603        unclosed("comment", startline, startcol);
604        if (dbg) dbgexit(); 
605        return;
606        
607      case '*':
608        if (in.match("/]"))
609          {
610          tree.add(new Comment(buf));
611          buf.reset();  
612          if (dbg) dbgexit(); 
613          return;
614          }
615        else
616          append(c);  
617        break;
618      
619      default:
620        append(c);
621      }
622    }
623  }
624
625void parseDeclaration() throws IOException
626  {
627  if (dbg) dbgenter(); 
628  int startline = in.getLine();
629  int startcol = in.getCol();
630
631  while (true)
632    {
633    c = in.read();      
634  
635    switch (c)
636      {
637      case EOF:
638        unclosed("declaration", startline, startcol);
639        if (dbg) dbgexit(); 
640        return;
641      
642      case '!':
643        if (in.match(']')) {
644          decl.add(new Decl(buf));
645          buf.reset();  
646          if (dbg) dbgexit(); 
647          return;
648          }
649        else{
650          append(c);
651          }
652        break;
653
654      //top level // and /* comments, ']' (close decl tag)
655      //is ignored within them
656      case '/':   
657        append(c);
658        c = in.read();
659        append(c);
660        if (c == '/') 
661          appendCodeSlashComment();
662        else if (c == '*') 
663          appendCodeStarComment();
664          break;        
665    
666      //close tags are ignored within them
667      case '"':     //strings outside of any comment
668        append(c);
669        appendCodeString();  
670        break;
671        
672      case '\'':
673        append(c);
674        appendCodeCharLiteral();
675        break;
676            
677      default:
678        append(c);
679      }
680    }
681
682  }
683
684void parseDirective() throws IOException
685  {
686  if (dbg) dbgenter(); 
687
688  int startline = in.getLine();
689  int startcol = in.getCol();
690
691  StringBuilder directives_buf = new StringBuilder(1024);
692
693  while (true)
694    {
695    c = in.read();      
696  
697    switch (c)
698      {
699      case EOF:
700        unclosed("directive", startline, startcol);
701        if (dbg) dbgexit(); 
702        return;
703        
704      case ']':
705        if (directives_buf.indexOf("<<<") >= 0)  {
706          parseHeredoc(directives_buf); 
707          }
708        else{/* other directives used at page-generation time */
709          addDirectives(directives_buf);
710          }
711          
712        if (dbg) dbgexit(); 
713        return;
714      
715      default:
716        directives_buf.append((char)c);
717      }
718    }
719
720  }
721
722//[a-zA-Z_\-0-9] == ( \w | - )
723static final Pattern directive_pat = Pattern.compile(
724  //foo = "bar baz" (embd. spaces)
725  "\\s*([a-zA-Z_\\-0-9]+)\\s*=\\s*\"((?:.|\r|\n)+?)\""  
726  + "|"
727  //foo = "bar$@#$" (no spaces) OR foo = bar (quotes optional)
728  + "\\s*([a-zA-Z_\\-0-9]+)\\s*=\\s*(\\S+)" 
729  );
730  
731    
732void addDirectives(StringBuilder directives_buf) throws TemplateParseException
733  {
734  if (dbg) {
735    dbgenter(); 
736    System.out.println("-------directives section--------");
737    System.out.println(directives_buf.toString());
738    System.out.println("-------end directives-------");
739    }
740  
741  String name, value;
742  try {
743    Matcher m = directive_pat.matcher(directives_buf);
744    while (m.find()) 
745      {
746      if (dbg) System.out.println(">>>>[0]->" + m.group() 
747        + "; [1]->" + m.group(1)  
748        + " [2]->" + m.group(2)  
749        + " [3]->" + m.group(3)  
750        + " [4]->" + m.group(4));
751        
752      name = m.group(1) != null ? m.group(1).toLowerCase() :
753                    m.group(3).toLowerCase();
754      value = m.group(2) != null ? m.group(2).toLowerCase() :
755                     m.group(4).toLowerCase();
756
757      if (name.equals(d_remove_initial_emptylines)) {
758        directives.put(name, value.replace("\"|'",""));       
759        } 
760      else if (name.equals(d_remove_all_emptylines)) {
761        directives.put(name, value.replace("\"|'",""));       
762        } 
763      //else if .... other directives here as needed....
764      else 
765        throw new Exception("Do not understand directive: " + m.group());
766      }
767    if (dbg) System.out.println("Added directives: " + directives);
768    }
769  catch (Exception e) {
770    throw new TemplateParseException("File: " + inputFile.getAbsolutePath() 
771                  + ";\n" + e.toString());
772    }
773
774  if (dbg) dbgexit(); 
775  }
776
777void parseIncludeFile() throws IOException
778  {
779  if (dbg) dbgenter(); 
780
781  int startline = in.getLine();
782  int startcol = in.getCol();
783  String option = null;
784  
785  while (true)
786    {
787    c = in.read();      
788  
789    switch (c)
790      {
791      case EOF:
792        unclosed("include-file", startline, startcol);
793        if (dbg) dbgexit(); 
794        return;
795        
796      case '[':
797        if (in.match('=')) {
798  //log.warn("Expressions cannot exist in file includes. Ignoring \"[=\"
799  //in [include-file... section starting at:", startline, startcol);
800  //instead of warn, we will error out. failing early is better.
801  //this does preclude having '[=' in the file name, but it's a good
802  //tradeoff
803          error("Expressions cannot exist in file includes. The offending static-include section starts at:", startline, startcol);
804          }
805        append(c);
806        break;
807      
808      case ']':
809        includeFile(buf, option); /* not added in the tree, just included in the stream */
810        buf.reset();  
811        if (dbg) dbgexit(); 
812        return;
813      
814      case 'o':
815        if (! in.match("ption"))
816          append(c);
817        else{
818          skipWS();
819          if (! in.match("=")) {
820            error("bad option parameter in file include: ", startline, startcol);
821            }
822          skipWS();
823          
824          int c2;
825          StringBuilder optionbuf = new StringBuilder();
826          while (true) {
827            c2 = in.read();
828            if (c2 == ']' || c2 == EOF || Character.isWhitespace(c2)) {   
829              in.unread();
830              break;
831              }
832            optionbuf.append((char)c2);
833            }
834          
835          option = optionbuf.toString();
836          //System.out.println(option);
837          } //else
838        break;
839  
840      default:
841        append(c);
842      }
843    }
844  }
845
846void parseIncludeDecl() throws IOException
847  {
848  if (dbg) dbgenter(); 
849
850  int startline = in.getLine();
851  int startcol = in.getCol();
852  String option = null;
853  
854  while (true)
855    {
856    c = in.read();      
857  
858    switch (c)
859      {
860      case EOF:
861        unclosed("include-decl", startline, startcol);
862        if (dbg) dbgexit(); 
863        return;
864        
865      case '[':
866        if (in.match('=')) {
867    //log.warn("Expressions cannot exist in file includes. Ignoring \"[=\" in [include-static... section starting at:", startline, startcol);
868    //we will throw an exception. failing early is better. this
869    //does preclude having '[=' in the file name, but it's a good tradeoff
870          error("Expressions cannot exist in include-decl. The offending static-include section starts at:", startline, startcol);
871          }
872        append(c);
873        break;
874      
875      case ']':
876        IncludeDecl i = new IncludeDecl(buf);
877        if (option != null)
878          i.setOption(option);
879        inc_decl.add(i);
880        buf.reset();  
881        if (dbg) dbgexit(); 
882        return;
883      
884      case 'o':
885        if (! in.match("ption"))
886          append(c);
887        else{
888          skipWS();
889          if (! in.match("=")) {
890            error("bad option parameter in include-code: ", startline, startcol);
891            }
892          skipWS();
893          
894          int c2;
895          StringBuilder optionbuf = new StringBuilder();
896          while (true) {
897            c2 = in.read();
898            if (c2 == ']' || c2 == EOF || Character.isWhitespace(c2)) {   
899              in.unread();
900              break;
901              }
902            optionbuf.append((char)c2);
903            }
904          
905          option = optionbuf.toString();
906          //System.out.println(option);
907          } //else
908        break;
909  
910      default:
911        append(c);
912      }
913    }
914  }
915
916
917//we need to parse imports seperately because they go outside
918//a class declaration (and [!...!] goes inside a class)
919//import XXX.*;
920//class YYY {
921//[!....stuff from here ....!]
922//...
923void parseImport() throws IOException
924  {
925  if (dbg) dbgenter(); 
926
927  int startline = in.getLine();
928  int startcol = in.getCol();
929
930  while (true)
931    {
932    c = in.read();      
933  
934    switch (c)
935      {
936      case EOF:
937        unclosed("import", startline, startcol);
938        if (dbg) dbgexit(); 
939        return;
940      
941      case '\n':
942        imps.add(new Import(buf));
943        buf.reset();
944        break;
945        
946      case ']':
947        imps.add(new Import(buf));
948        buf.reset();  
949        if (dbg) dbgexit(); 
950        return;
951      
952      default:
953        append(c);
954      }
955    }
956  }
957
958/*
959Called when // was read at the top level inside a code block. Appends
960the contents of a // comment to the buffer (not including the trailing
961newline)
962*/
963void appendCodeSlashComment() throws IOException
964  {
965  if (dbg) dbgenter();
966  
967  while (true) 
968    {
969    c = in.read();
970    
971    if (c == EOF)
972      break;
973  
974    //do not append \r, \r\n, or \n, that finishes the // comment
975    //we need that newline to figure out if the next line is a hash
976    //line
977    if (c == '\r') {
978      in.unread();
979      break;
980      }
981    
982    if (c == '\n') {
983      in.unread();
984      break;  
985      }
986
987    append(c);
988    }
989  
990  if (dbg) dbgread("CodeSLASHComment Finished: Buffer=" + StringUtil.viewableAscii(buf.toString()));
991  if (dbg) dbgexit();
992  }
993
994/*
995Called when /* was read at the top level inside a code block. Appends
996the contents of a /*comment to the buffer. (not including any trailing
997newline or spaces)
998*/
999void appendCodeStarComment() throws IOException
1000  {
1001  if (dbg) dbgenter(); 
1002  
1003  while (true) 
1004    {
1005    c = in.read();  
1006
1007    if (c == EOF)
1008      break;
1009  
1010    append(c);
1011    
1012    if (c == '*') 
1013      {
1014      if (in.match('/')) {
1015        append('/');
1016        break;
1017        }
1018      }
1019    }
1020
1021  if (dbg) dbgread("CodeSTARComment Finished: Buffer=" + StringUtil.viewableAscii(buf.toString()));
1022  if (dbg) dbgexit(); 
1023  }
1024
1025/*
1026Called (outside of any comments in the code block) when: 
1027--> parseCode()
1028     ... "
1029         ^ (we are here)
1030*/
1031void appendCodeString() throws IOException
1032  {
1033  if (dbg) dbgenter(); 
1034
1035  int startline = in.getLine();
1036  int startcol = in.getCol();
1037
1038  while (true) 
1039    {
1040    c = in.read();
1041  
1042    if (c == EOF || c == '\r' || c == '\n')
1043      unclosed("string literal", startline, startcol);
1044  
1045    append(c);
1046  
1047    if (c == '\\') {
1048      c = in.read();
1049      if (c == EOF)
1050        unclosed("string literal", startline, startcol);
1051      else {
1052        append(c);
1053        continue;   //so \" does not hit the if below and break
1054        }
1055      }
1056    
1057    if (c == '"')
1058      break;
1059    }
1060
1061  if (dbg) dbgread("appendCodeString Finished: Buffer=" + StringUtil.viewableAscii(buf.toString()));
1062  if (dbg) dbgexit(); 
1063  }
1064
1065
1066/*
1067Called (outside of any comments in the code block) when: 
1068--> parseCode()
1069     ... '
1070         ^ (we are here)
1071*/
1072void appendCodeCharLiteral() throws IOException
1073  {
1074  if (dbg) dbgenter(); 
1075
1076  int startline = in.getLine();
1077  int startcol = in.getCol();
1078
1079  while (true) 
1080    {
1081    c = in.read();
1082  
1083    if (c == EOF || c == '\r' || c == '\n')
1084      unclosed("char literal", startline, startcol);
1085  
1086    append(c);
1087  
1088    if (c == '\\') {
1089      c = in.read();
1090      if (c == EOF)
1091        unclosed("char literal", startline, startcol);
1092      else {
1093        append(c);
1094        continue;   //so \' does not hit the if below and break
1095        }
1096      }
1097    
1098    if (c == '\'')
1099      break;
1100    }
1101
1102  if (dbg) dbgread("appendCodeCharLiteral Finished: Buffer=" + StringUtil.viewableAscii(buf.toString()));
1103  if (dbg) dbgexit(); 
1104  }
1105
1106
1107/*
1108Reads from the current position till the first nonwhitespace char, EOF or
1109newline is encountered. Reads are into the whitespace buffer. does not
1110consume the character past the non-whitespace character and does
1111NOT read multiple lines of whitespace.
1112*/
1113void readToFirstNonWS() throws IOException 
1114  {
1115  wsbuf.reset();
1116
1117  while (true)
1118    {
1119    c = in.read();
1120  
1121    if (c == '\r' || c == '\n')
1122      break;
1123      
1124    if (c == EOF || ! Character.isWhitespace(c))
1125      break;
1126  
1127    wsbuf.append((char)c);
1128    }
1129    
1130  in.unread();
1131  }
1132
1133//skip till end of whitespace or EOF. does not consume any chars past 
1134//the whitespace.
1135void skipWS() throws IOException
1136  {
1137  int c2 = EOF;
1138  while (true) {
1139    c2 = in.read();
1140    if (c2 == EOF || ! Character.isWhitespace(c2)) {
1141      in.unread();
1142      break;
1143      }
1144    } 
1145  }
1146  
1147//skips to the end of line if the rest of the line is (from the current
1148//position), all whitespace till the end. otherwise, does not change 
1149//current position. consumes trailing newlines (if present) when reading 
1150//whitespace.
1151void skipIfWhitespaceToEnd() throws IOException
1152  {
1153  int count = 0;
1154  
1155  while (true) 
1156    {
1157    c = in.read();
1158      count++;
1159
1160    if (c == '\r') {
1161      in.match('\n');
1162      return;
1163      }
1164      
1165    if (c == '\n' || c == EOF)
1166      return;
1167      
1168    if (! Character.isWhitespace(c))
1169      break;
1170      }
1171
1172  in.unread(count);
1173  }
1174
1175//not used anymore but left here for potential future use. does not
1176//consume the newline (if present)
1177void skipToLineEnd() throws IOException 
1178  {
1179    while (true) 
1180      {
1181      int c = in.read();
1182      if (c == EOF) {
1183        in.unread();
1184      break;
1185        }
1186      if (c == '\n' || c == '\r') { 
1187        in.unread();
1188        break;
1189        }
1190      }
1191    }
1192
1193String quote(final char c) 
1194  {
1195    switch (c)
1196      {
1197      case '\r':
1198            return "\\r";
1199            
1200      case '\n':
1201            return "\\n";
1202 
1203    case '\"':
1204      //can also say: new String(new char[] {'\', '"'})
1205            return "\\\"";    //--> \"
1206 
1207    case '\\':
1208            return "\\\\";
1209    
1210      default:
1211        return String.valueOf(c);
1212      }
1213    }
1214
1215//======= util and debug methods ==========================
1216String methodName(int framenum)
1217  {
1218  StackTraceElement ste[] = new Exception().getStackTrace();
1219  //get method that called us, we are ste[0]
1220  StackTraceElement st = ste[framenum];
1221  String file = st.getFileName();
1222  int line = st.getLineNumber();
1223  String method = st.getMethodName();
1224  String threadname = Thread.currentThread().getName();
1225  return method + "()";   
1226  }
1227
1228void dbgenter() {
1229  System.out.format("%s-->%s\n", StringUtil.repeat('\t', dbgtab++), methodName(2));
1230  }
1231  
1232void dbgexit() {
1233  System.out.format("%s<--%s\n", StringUtil.repeat('\t', --dbgtab), methodName(2));
1234  }
1235
1236void dbgread(String str) {
1237  System.out.format("%s %s\n", StringUtil.repeat('\t', dbgtab), StringUtil.viewableAscii(str));
1238  }
1239
1240void dbgread(String str, List list) {
1241  System.out.format("%s %s: ", StringUtil.repeat('\t', dbgtab), StringUtil.viewableAscii(str));
1242  for (int n = 0; n < list.size(); n++) {
1243    System.out.print( StringUtil.viewableAscii( (String)list.get(n) ) );
1244    }
1245  System.out.println("");
1246  }
1247
1248void dbgread(char c) {
1249  System.out.format("%s %s\n", StringUtil.repeat('\t', dbgtab), StringUtil.viewableAscii(c));
1250  }
1251
1252void dbgread(CharArrayWriter buf) {
1253  System.out.format("%s %s\n", StringUtil.repeat('\t', dbgtab), StringUtil.viewableAscii(buf.toString()));
1254  }
1255
1256void unclosed(String blockname, int startline, int startcol) throws IOException
1257  {
1258  throw new IOException(blockname + " tag not closed.\nThis tag was possibly opened in: \nFile:"
1259    + inputFile + ", line:" 
1260    + startline + " column:" + startcol +
1261    ".\nCurrent line:" + in.getLine() + " column:" + in.getCol());  
1262  }
1263
1264void error(String msg, int line, int col) throws IOException
1265  {
1266  throw new IOException("Error in File:" + inputFile + " Line:" + line + " Col:" + col + " " + msg);  
1267  }
1268
1269void error(String msg) throws IOException
1270  {
1271  throw new IOException("Error in File:" + inputFile + " " + msg);  
1272  }
1273
1274//============== Non Parsing methods ================================
1275void o(Object str) {
1276  out.print(str);
1277  }
1278
1279void ol(Object str) {
1280  out.println(str); 
1281  }
1282
1283void ol() {
1284  out.println();
1285  }
1286  
1287
1288/* 
1289include an external file whose contents will be rendered as part of the page.
1290*/ 
1291void includeFile(CharArrayWriter buf, String option) throws IOException
1292  {
1293  String str;
1294  
1295  if (dbg) dbgread("<new INCLUDE-FILE> "); 
1296  str = removeLeadingTrailingQuote(buf.toString().trim());
1297  
1298  File includeFile = null;
1299  File parentDir = inputFile.getParentFile();
1300  if (parentDir == null) {
1301    parentDir = new File(".");
1302    }
1303
1304  if (str.startsWith("/"))
1305    includeFile = new File(str);
1306  else
1307    includeFile = new File(parentDir, str);
1308        
1309  //System.out.println(">>>>>>>>>> f="+f +";root="+contextRoot);
1310      
1311  if (! includeFile.exists()) {
1312    throw new IOException("Include file does not exist: " + includeFile.getCanonicalPath());
1313    }
1314
1315  if (circularityTrack.contains(includeFile.getAbsolutePath())) {
1316     throw new IOException("Circularity detected when including: " + includeFile.getCanonicalPath() + "\nAlready included the following files: " + circularityTrack);
1317    }
1318
1319  tree.add(new MollyComment(
1320    "//>>>START INCLUDE from: " + includeFile.getAbsolutePath()));
1321    
1322  /*
1323    TemplateParser pp = new TemplateParser(contextRoot, includeFile, out, classname, log);
1324    pp.includeMode().parse();  //writes to out
1325  */
1326  
1327  in.insertIntoStream(includeFile);
1328
1329  /* this is printed immediately before the inserted contents can be processed, so don't add this */
1330  /*
1331  tree.add(new MollyComment(
1332    "//>>>END INCLUDE from: " + includeFile.getAbsolutePath()));
1333  */
1334  
1335  circularityTrack.add(includeFile.getAbsolutePath());
1336  }
1337
1338  
1339void writePage() throws IOException
1340  { 
1341  if (! includeMode)
1342    {
1343    //create a appropriate PrintWriter based on either the default
1344    //jave encoding or the page specified java encoding
1345    //the java source file will be written out in this encoding
1346  
1347    FileOutputStream  fout = new FileOutputStream(outputFile);
1348    OutputStreamWriter  fw   = new OutputStreamWriter(fout, TemplatePage.DEFAULT_ENCODING);
1349        
1350    out = new PrintWriter(new BufferedWriter(fw));
1351    }
1352    
1353  if (! includeMode) 
1354    {
1355    writePackage();
1356    writeImports();
1357    
1358    o ("public class ");
1359    o (classname);
1360    ol(" extends fc.util.pagetemplate.TemplatePage");
1361    ol("{");
1362    }
1363
1364  writeFields();
1365
1366  if (! includeMode) {
1367    writeConstructor();
1368    }
1369    
1370  writeMethods();
1371  
1372  if (! includeMode) {
1373    ol("}");
1374    }
1375  }
1376
1377void writePackage()
1378  {
1379  o ("package ");
1380  o (packagename);
1381  ol(";");
1382  ol();
1383  }
1384  
1385void writeImports() throws IOException
1386  {
1387  ol("import java.io.*;");
1388  ol("import java.util.*;");
1389  ol("import java.sql.*;");
1390  for (int n = 0; n < imps.size(); n++) {
1391    ((Element)imps.get(n)).render();
1392    ol();
1393    }
1394  ol();
1395  }
1396
1397void writeFields()
1398  {
1399  }
1400
1401void writeConstructor()
1402  {
1403  }
1404
1405void writeMethods() throws IOException
1406  {
1407  writeDeclaredMethods();
1408  writeIncludedMethods();
1409  writeRenderMethod();
1410  }
1411  
1412void writeDeclaredMethods() throws IOException
1413  {
1414  for (int n = 0; n < decl.size(); n++) {
1415    ((Element)decl.get(n)).render();
1416    }
1417  
1418  if (decl.size() > 0)
1419    ol();
1420  }
1421
1422void writeIncludedMethods() throws IOException
1423  {
1424  for (int n = 0; n < inc_decl.size(); n++) {
1425    ((Element)inc_decl.get(n)).render();
1426    }
1427    
1428  if (inc_decl.size() > 0)
1429    ol();
1430  }
1431
1432void writeRenderMethod() throws IOException
1433  {
1434  if  (! includeMode) {
1435    writeRenderTop();
1436    }
1437  
1438  /* remove leading emptylines if directed */
1439  if (directives.containsKey(d_remove_initial_emptylines)) 
1440    {
1441    if (dbg) System.out.println("[d_remove_initial_emptylines] directive found, removing leading whitepace");
1442    boolean white_space = true;
1443    
1444    //have to use iterator when removing while transversing
1445    Iterator it = tree.iterator();
1446    while (it.hasNext()) 
1447      {
1448      Element e = (Element) it.next();
1449      if (e instanceof Text) 
1450        {
1451        Text t = (Text) e;
1452        if (t.isOnlyWhitespace()) {
1453          it.remove();
1454          }
1455        }
1456      else{
1457        if (! (e instanceof Comment || e instanceof Decl || e instanceof MollyComment)) {
1458          //the initial whitespace mode is not applicable since some other declaration seen
1459          break;
1460          }
1461        }
1462      }
1463    }
1464
1465  /* remove all empty lines if directed */
1466  if (directives.containsKey(d_remove_all_emptylines)) 
1467    {
1468    if (dbg) System.out.println("[d_remove_all_emptylines] directive found, removing leading whitepace");
1469    boolean white_space = true;
1470    
1471    //have to use iterator when removing while transversing
1472    Iterator it = tree.iterator();
1473    while (it.hasNext()) 
1474      {
1475      Element e = (Element) it.next();
1476      if (e instanceof Text) 
1477        {
1478        Text t = (Text) e;
1479        if (t.isOnlyWhitespace()) {
1480          it.remove();
1481          }
1482        }
1483      }
1484    }
1485
1486  
1487  for (int n = 0; n < tree.size(); n++) {
1488    ((Element)tree.get(n)).render();
1489    }
1490    
1491  if (! includeMode) {
1492    writeRenderBottom();
1493    }
1494      
1495  }
1496  
1497void writeRenderTop() throws IOException
1498  {
1499  ol("public void render(PrintWriter out) throws Exception");
1500  ol("\t{");
1501  ol();
1502  }
1503
1504void writeRenderBottom() throws IOException
1505  {
1506  ol();
1507  o("\t");
1508  ol("out.flush();");
1509  ol("out.close();");
1510  ol("\t} //~render end");
1511  }
1512
1513
1514/*
1515int tabcount = 1;
1516String tab = "\t";
1517void tabInc() {
1518  tab = StringUtil.repeat('\t', ++tabcount);
1519  }
1520void tabDec() {
1521  tab = StringUtil.repeat('\t', --tabcount);
1522  }
1523*/
1524
1525abstract class Element {
1526  abstract void render() throws IOException;
1527  //text, include etc., implement this as needed. 
1528  void addExp(Exp e) {  
1529    throw new RuntimeException("Internal error: not implemented by this object"); 
1530    }
1531  }
1532    
1533//this should NOT be added to the tree directly but added to Text or Hash
1534//via the addExp() method. This is because exps must be printed inline
1535class Exp extends Element
1536  {
1537  String str;
1538  
1539  Exp(CharArrayWriter buf) {
1540    this.str = buf.toString();
1541    if (dbg) dbgread("<new EXP> "+ str); 
1542    }
1543
1544  void render() {
1545    o("out.print  (");
1546    o(str);
1547    ol(");");
1548    }
1549    
1550  public String toString() {
1551    return "Exp: [" + str + "]";
1552    }
1553  }
1554  
1555class Text extends Element
1556  {
1557  String  offset_space;
1558  final   List list = new ArrayList();
1559
1560  //each text section is parsed by a text node. Within EACH text
1561  //node, we split it's contained text into separate lines and
1562  //generate code to print each line with a "out.println(...)"
1563  //statement. This maintains the same source order as the molly
1564  //page. If we munge together everything and print all of it's
1565  //contents with just one out.println(...)" statement, we would
1566  //get one large line with embedded \n and that would make
1567  //things more difficult to co-relate with the source file.
1568
1569  Text(final String offset, final CharArrayWriter b) 
1570    {
1571    if (offset == null)
1572      offset_space = "\t";
1573    else
1574      offset_space = "\t" + offset;
1575  
1576    final char[] buf = b.toCharArray();
1577
1578    boolean prevWasCR = false;
1579    //jdk default is 32. we say 256. not too large, maybe
1580    //less cache pressure. not too important, gets resized
1581    //as needed anyway.
1582    final CharArrayWriter tmp = new CharArrayWriter(256);
1583    
1584    for (int i=0, j=1; i < buf.length; i++, j++) 
1585      {
1586      char c = buf[i];   
1587      if (j == buf.length) {
1588        tmp.append(quote(c));
1589        list.add(tmp.toString());
1590        tmp.reset();
1591        }
1592      else if (c == '\n') {
1593        tmp.append(quote(c));
1594        if (! prevWasCR) {
1595          list.add(tmp.toString());
1596          tmp.reset();
1597          }
1598        }
1599      else if (c == '\r') {
1600        tmp.append(quote(c));
1601        list.add(tmp.toString());
1602        tmp.reset();
1603        prevWasCR = true;
1604        }
1605      else{
1606        tmp.append(quote(c));
1607        prevWasCR = false;
1608        }
1609      }
1610
1611    if (dbg) {
1612      String classname = getClass().getName();
1613      dbgread("<new " + classname.substring(classname.indexOf("$")+1,classname.length()) + ">",list); 
1614      }
1615    }
1616
1617  Text(CharArrayWriter b) 
1618    {
1619    this(null, b);
1620    }
1621    
1622  void addExp(Exp e)
1623    {
1624    list.add(e);
1625    }
1626
1627  void render() 
1628    {
1629    for (int i=0; i<list.size(); i++) 
1630      {
1631      Object obj = list.get(i); //can be String or Exp
1632      if (obj instanceof Exp) {
1633        o(offset_space);
1634        ((Exp)obj).render();
1635        }
1636      else{
1637        o(offset_space);
1638        o("out.print  (\"");
1639        o(obj);
1640        ol("\");"); 
1641        }
1642      }
1643    } //render
1644
1645  //a newline is actuall '\' and '\n' since it's fed to out (..)
1646  //to check for whitepace we need to check for '\', 'n', etc
1647  boolean isOnlyWhitespace() 
1648    {
1649    for (int i = 0; i < list.size(); i++) 
1650      {
1651      Object obj = list.get(i); //can be String or Exp
1652      if (obj instanceof Exp) {
1653        return false;
1654        }
1655      else{
1656        String s = (String) obj;
1657        //dont even ask my why \\\\, fucking ridiculous
1658        if (! s.matches("^(\\\\n|\\\\r|\\\\t| )*$")) {
1659          return false;
1660          }
1661        }
1662      }
1663      
1664    return true;
1665    }
1666    
1667  public String toString() {
1668    StringBuilder buf = new StringBuilder();
1669    buf.append("Text: [");
1670    for (int n = 0; n < list.size(); n++) {
1671      buf.append(StringUtil.viewableAscii(String.valueOf(list.get(n))));
1672      }
1673    buf.append("]");
1674    return buf.toString();
1675    }
1676  
1677  }
1678
1679class Hash extends Text
1680  {
1681  Hash(final String offset, final CharArrayWriter b) 
1682    {
1683    super(offset, b);
1684    }
1685
1686  //same as super.render() except for j == list.size() o/ol() below
1687  void render() 
1688    {
1689    for (int i=0, j=1; i<list.size(); i++, j++) 
1690      {
1691      Object obj = list.get(i); //can be String or Exp
1692      if (obj instanceof Exp) {
1693        o(offset_space);
1694        ((Exp)obj).render();
1695        }
1696      else{
1697        o(offset_space);
1698        o("out.print  (\"");
1699        o(obj);
1700        
1701        if (j == list.size()) 
1702          o ("\");");
1703        else
1704          ol("\");"); 
1705        }
1706      }
1707    } //render
1708
1709  public String toString() {
1710    return "Hash: " + list;
1711    }
1712  }
1713
1714class Heredoc extends Text
1715  {
1716  Heredoc(final CharArrayWriter buf) 
1717    {
1718    super(null, buf);
1719    }
1720
1721  //override, exp cannot be added to heredoc sections
1722  void addExp(Exp e)
1723    {
1724    throw new IllegalStateException("Internal implementation error: this method should not be called for a Heredoc object");
1725    }
1726    
1727  void render() 
1728    {
1729    for (int i=0, j=1; i<list.size(); i++, j++) 
1730      {
1731      Object obj = list.get(i); 
1732      o(offset_space);
1733      o("out.print  (\"");
1734      o(obj);
1735      ol("\");"); 
1736      }
1737    } //render
1738
1739  public String toString() {
1740    return "Heredoc: " + list;
1741    }
1742
1743  }
1744
1745
1746class Code extends Element
1747  {
1748  List list = new ArrayList();
1749  
1750  Code(CharArrayWriter b) 
1751    {
1752    //we split the code section into separate lines and 
1753    //print each line with a out.print(...). This maintains
1754    //the same source order as the molly page. If we munge together
1755    //everything, we would get one large line with embedded \n
1756    //and that would make things more difficult to co-relate.
1757    final char[] buf = b.toCharArray();
1758    CharArrayWriter tmp = new CharArrayWriter();
1759    for (int i=0, j=1; i < buf.length; i++, j++) {
1760      char c = buf[i];   
1761      if (j == buf.length) { //end of buffer
1762        tmp.append(c);
1763        list.add(tmp.toString());
1764        tmp.reset();
1765        }
1766      else if (c == '\n') {
1767        tmp.append(c);
1768        list.add(tmp.toString());
1769        tmp.reset();
1770        }
1771      else
1772        tmp.append(c);
1773      }
1774    if (dbg) {
1775      String classname = getClass().getName();
1776      dbgread("<new " + classname.substring(classname.indexOf("$")+1,classname.length()) + ">",list); 
1777      }
1778    }
1779
1780  void render() {
1781    for (int i = 0; i < list.size(); i++) {
1782      o('\t');
1783      o(list.get(i));
1784      }
1785    }
1786    
1787  public String toString() {
1788    return "Code: " + list;
1789    }
1790  }
1791
1792class Comment extends Element
1793  {
1794  String str;
1795  
1796  Comment(CharArrayWriter buf) {
1797    this.str = buf.toString();
1798    if (dbg) dbgread("<new COMMENT> "+ str); 
1799    }
1800
1801  void render() {
1802    //we don't print commented sections
1803    }
1804
1805  public String toString() {
1806    return "Comment: [" + str + "]";
1807    }
1808  }
1809
1810class Decl extends Code
1811  {
1812  Decl(CharArrayWriter buf) {
1813    super(buf);
1814    }
1815
1816  void render() {
1817    for (int i = 0; i < list.size(); i++) {
1818      o (list.get(i));
1819      }
1820    }
1821  }
1822
1823
1824
1825/* a molly mechanism to include an external file containing code and method
1826   declarations. These are typically commom utility methods and global
1827   vars. The included file is not parsed by the template parser... the contents
1828   are treated as if they were written directly inside a [!....!] block.
1829*/ 
1830class IncludeDecl extends Element
1831  {
1832  String str;
1833  String opt;
1834  
1835  IncludeDecl(CharArrayWriter buf) {
1836    if (dbg) dbgread("<new INCLUDE-DECL> "); 
1837    str = removeLeadingTrailingQuote(buf.toString().trim());
1838    }
1839  
1840  void setOption(String opt) {
1841    this.opt = opt;
1842    }
1843  
1844  void render() throws IOException
1845    {
1846    File f = null;
1847    File parentDir = inputFile.getParentFile();
1848    if (parentDir == null) {
1849      parentDir = new File(".");
1850      }
1851
1852    final int strlen = str.length();
1853    
1854    if (str.startsWith("\"") || str.startsWith("'")) 
1855      {
1856      if (strlen == 1) //just " or ' 
1857        throw new IOException("Bad include file name: " + str);
1858        
1859      str = str.substring(1, strlen);
1860      }
1861
1862    if (str.endsWith("\"") || str.endsWith("'")) 
1863      {
1864      if (strlen == 1) //just " or ' 
1865        throw new IOException("Bad include file name: " + str);
1866        
1867      str = str.substring(0, strlen-1);
1868      }
1869
1870    if (str.startsWith("/"))
1871      f = new File(str);
1872    else
1873      f = new File(parentDir, str);
1874        
1875    if (! f.exists()) {
1876      throw new IOException("Include file does not exist: " + f.getCanonicalPath());
1877      }
1878
1879    o("//>>>START INCLUDE DECLARTIONS from: ");
1880    o(f.getAbsolutePath());
1881    ol();
1882        
1883    o(IOUtil.inputStreamToString(new FileInputStream(f)));
1884  
1885    o("//>>>END INCLUDE DECLARATIONS from: ");
1886    o(f.getAbsolutePath());
1887    ol();
1888    
1889    //circularities are tricky, later
1890    //includeMap.put(pageloc, f.getCanonicalPath());
1891    }
1892
1893  public String toString() {
1894    return "IncludeDecl: [" + str + "; options: " + opt + "]";
1895    }
1896  }
1897
1898class Import extends Code
1899  {
1900  Import(CharArrayWriter buf) {
1901    super(buf);
1902    }
1903
1904  void render() {
1905    for (int i = 0; i < list.size(); i++) {
1906      o (list.get(i));
1907      }
1908    }
1909  }
1910
1911class MollyComment extends Element
1912  {
1913  String str;
1914  
1915  MollyComment(String str) {
1916    this.str = str;
1917    if (dbg) dbgread("<new MollyComment> "+ str); 
1918    }
1919
1920  void render() {
1921    ol(str);
1922    }
1923    
1924  public String toString() {
1925    return "MollyComment: [" + str + "]";
1926    }
1927  }
1928  
1929/**
1930removes starting and trailing single/double quotes. used by the
1931include/forward render methods only, NOT used while parsing.
1932*/
1933private static String removeLeadingTrailingQuote(String str)
1934  {
1935  if (str == null)
1936    return str;
1937
1938  if ( str.startsWith("\"") || str.startsWith("'") )  {
1939    str = str.substring(1, str.length());
1940    }
1941
1942  if ( str.endsWith("\"") || str.endsWith("'") ) {
1943    str = str.substring(0, str.length()-1); 
1944    }
1945
1946  return str;
1947  }
1948
1949//===============================================
1950
1951public static void main (String args[]) throws IOException
1952  {
1953  Args myargs = new Args(args);
1954  myargs.setUsage("java " + myargs.getMainClassName() 
1955    + "\n"
1956      + "Required params:\n"
1957    + "     -classname output_class_name\n" 
1958    + "     -in        input_page_file\n"
1959    + "\nOptional params:\n" 
1960    + "     -encoding    <page_encoding>\n"
1961    + "     -out <output_file_name>\n"
1962    + "        the output file is optional and defaults to the standard out if not specified."
1963    );
1964  //String encoding = myargs.get("encoding", Page.DEFAULT_ENCODING);
1965
1966  File input     = new File(myargs.getRequired("in"));
1967
1968  PrintWriter output;
1969  
1970  if (myargs.get("out") != null)
1971    output = new PrintWriter(new FileWriter(myargs.get("out")));
1972  else
1973    output = new PrintWriter(new OutputStreamWriter(System.out));
1974    
1975  TemplateParser parser = new TemplateParser(input, output, myargs.getRequired("classname"), Log.getDefault());
1976  parser.parse();
1977  }
1978
1979}