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    
006    package fc.util.pagetemplate;
007    
008    import java.io.*;
009    import java.util.*;
010    import java.util.regex.*;
011    import fc.io.*;
012    import fc.util.*;
013    
014    /*
015    NOTES
016    
017    Code blocks of the form 
018     [...] 
019    cause problems with java arrays
020    
021    String[] or foo[4] etc.., craps out. So we need to use 
022     [[...]] 
023    for the molly code blocks
024    
025    The 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    
028    Read www.mollypages.org/page/grammar/index.mp for a intro to parsing
029    */
030    
031    /**
032    Parses a template page and writes out the corresponding java file to the specified
033    output. The parser and scanner is combined into one class here for simplicity (a
034    seperate scanner is overkill for a simple LL(1) grammar such as page templates).
035    
036    @author hursh jain
037    */
038    public final class TemplateParser
039    {
040    private static final boolean dbg    = false;
041    private static final int     EOF    = -1;
042    private              int     dbgtab = 0;
043    
044    String           classname;
045    String         packagename = TemplatePage.PACKAGE_NAME;
046    TemplateReader   in;
047    PrintWriter      out;
048    Log              log;
049    File             inputFile;
050    File             outputFile;
051    boolean          includeMode = false;
052    
053    //Read data
054    //we use these since stringbuffer/builders do not have a clear/reset function
055    CharArrayWriter buf = new CharArrayWriter(4096);
056    CharArrayWriter wsbuf = new CharArrayWriter(32);  // ^(whitespace)* 
057    int c = EOF;
058    
059    //PageData
060    List  decl       = new ArrayList();     //declarations
061    List  inc_decl       = new ArrayList();     //external included declarations
062    List  imps           = new ArrayList();     //imports
063    List  tree           = new ArrayList();     //code, exp, text etc.
064    Map   directives     = new HashMap();       //page options
065    Set   circularityTrack = new HashSet();   //track pages already included to stop circular refs
066    
067    /** The name of the  ("remove-initial-whitespace") directive */
068    public static String d_remove_initial_emptylines = "remove-initial-emptylines";
069    /** The name of the  ("remove-all-emptylines") directive */
070    public static String d_remove_all_emptylines = "remove-all-emptylines";
071    
072    /* 
073    This constructor for internal use.
074    
075    The parser can be invoked recursively to parse included files as
076    well..that's what the includeMode() does (and this construtor is invoked
077    when including). When including, we already have a output writer
078    created, we use that writer (instead of creating a new one based on
079    src_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    */
086    private TemplateParser(File input, PrintWriter outputWriter, String classname, Log log) 
087    throws 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    /**
100    Creates 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    */
108    public TemplateParser(File input, File output, String classname, Log log) 
109    throws 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    
120    void append(final int c)
121      {
122      Argcheck.istrue(c >= 0, "Internal error: recieved c=" + c);
123      buf.append((char)c);
124      }
125    
126    void append(final char c)
127      {
128      buf.append(c);
129      }
130    
131    void append(final String str)
132      {
133      buf.append(str);
134      }
135    
136    /* not used anymore */
137    TemplateParser includeMode()
138      {
139      includeMode = true;
140      return this;
141      }
142    
143    /**
144    Parses the page. If the parse is successful, the java source will be
145    generated.
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    */
151    public 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.
165    private Text newTextNode()
166      {
167      Text text = new Text(buf);
168      tree.add(text);
169      buf.reset();
170      return text;
171      }
172      
173    void 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      
278    void 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    
377    void 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..
490    FOO
491    */
492    void 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    /*
547    Text is the parent node for the expression. A new expression is parsed,
548    created and added to the text object by this method
549    */
550    void 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    
589    void 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    
625    void 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    
684    void 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 | - )
723    static 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        
732    void 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    
777    void 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    
846    void 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    //...
923    void 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    /*
959    Called when // was read at the top level inside a code block. Appends
960    the contents of a // comment to the buffer (not including the trailing
961    newline)
962    */
963    void 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    /*
995    Called when /* was read at the top level inside a code block. Appends
996    the contents of a /*comment to the buffer. (not including any trailing
997    newline or spaces)
998    */
999    void 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    /*
1026    Called (outside of any comments in the code block) when: 
1027    --> parseCode()
1028         ... "
1029             ^ (we are here)
1030    */
1031    void 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    /*
1067    Called (outside of any comments in the code block) when: 
1068    --> parseCode()
1069         ... '
1070             ^ (we are here)
1071    */
1072    void 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    /*
1108    Reads from the current position till the first nonwhitespace char, EOF or
1109    newline is encountered. Reads are into the whitespace buffer. does not
1110    consume the character past the non-whitespace character and does
1111    NOT read multiple lines of whitespace.
1112    */
1113    void 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.
1135    void 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.
1151    void 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)
1177    void 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    
1193    String 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 ==========================
1216    String 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    
1228    void dbgenter() {
1229      System.out.format("%s-->%s\n", StringUtil.repeat('\t', dbgtab++), methodName(2));
1230      }
1231      
1232    void dbgexit() {
1233      System.out.format("%s<--%s\n", StringUtil.repeat('\t', --dbgtab), methodName(2));
1234      }
1235    
1236    void dbgread(String str) {
1237      System.out.format("%s %s\n", StringUtil.repeat('\t', dbgtab), StringUtil.viewableAscii(str));
1238      }
1239    
1240    void 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    
1248    void dbgread(char c) {
1249      System.out.format("%s %s\n", StringUtil.repeat('\t', dbgtab), StringUtil.viewableAscii(c));
1250      }
1251    
1252    void dbgread(CharArrayWriter buf) {
1253      System.out.format("%s %s\n", StringUtil.repeat('\t', dbgtab), StringUtil.viewableAscii(buf.toString()));
1254      }
1255    
1256    void 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    
1264    void 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    
1269    void error(String msg) throws IOException
1270      {
1271      throw new IOException("Error in File:" + inputFile + " " + msg);  
1272      }
1273    
1274    //============== Non Parsing methods ================================
1275    void o(Object str) {
1276      out.print(str);
1277      }
1278    
1279    void ol(Object str) {
1280      out.println(str); 
1281      }
1282    
1283    void ol() {
1284      out.println();
1285      }
1286      
1287    
1288    /* 
1289    include an external file whose contents will be rendered as part of the page.
1290    */ 
1291    void 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      
1339    void 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    
1377    void writePackage()
1378      {
1379      o ("package ");
1380      o (packagename);
1381      ol(";");
1382      ol();
1383      }
1384      
1385    void 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    
1397    void writeFields()
1398      {
1399      }
1400    
1401    void writeConstructor()
1402      {
1403      }
1404    
1405    void writeMethods() throws IOException
1406      {
1407      writeDeclaredMethods();
1408      writeIncludedMethods();
1409      writeRenderMethod();
1410      }
1411      
1412    void 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    
1422    void 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    
1432    void 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      
1497    void writeRenderTop() throws IOException
1498      {
1499      ol("public void render(PrintWriter out) throws Exception");
1500      ol("\t{");
1501      ol();
1502      }
1503    
1504    void 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    /*
1515    int tabcount = 1;
1516    String tab = "\t";
1517    void tabInc() {
1518      tab = StringUtil.repeat('\t', ++tabcount);
1519      }
1520    void tabDec() {
1521      tab = StringUtil.repeat('\t', --tabcount);
1522      }
1523    */
1524    
1525    abstract 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
1535    class 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      
1555    class 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    
1679    class 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    
1714    class 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    
1746    class 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    
1792    class 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    
1810    class 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    */ 
1830    class 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    
1898    class 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    
1911    class 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    /**
1930    removes starting and trailing single/double quotes. used by the
1931    include/forward render methods only, NOT used while parsing.
1932    */
1933    private 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    
1951    public 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    }