[Templates-cvs] cvs commit: TT3/lib/Template Parser.pm

cvs@template-toolkit.org cvs@template-toolkit.org
Fri, 03 Dec 2004 11:13:43 +0000


cvs         04/12/03 11:13:43

  Modified:    lib/Template Parser.pm
  Log:
  * extracted all error messages out into $ERROR package hash
  * added parse_path() and parse_static_path() methods
  * deprecated some old methods
  
  Revision  Changes    Path
  1.18      +398 -255  TT3/lib/Template/Parser.pm
  
  Index: Parser.pm
  ===================================================================
  RCS file: /template-toolkit/TT3/lib/Template/Parser.pm,v
  retrieving revision 1.17
  retrieving revision 1.18
  diff -u -r1.17 -r1.18
  --- Parser.pm	2004/12/02 16:46:34	1.17
  +++ Parser.pm	2004/12/03 11:13:42	1.18
  @@ -18,7 +18,7 @@
   #   modify it under the same terms as Perl itself.
   #
   # REVISION
  -#   $Id: Parser.pm,v 1.17 2004/12/02 16:46:34 abw Exp $
  +#   $Id: Parser.pm,v 1.18 2004/12/03 11:13:42 abw Exp $
   #
   #========================================================================
   
  @@ -29,7 +29,7 @@
   use Template::Base;
   use base qw( Template::Base );
   
  -our $VERSION = sprintf("%d.%02d", q$Revision: 1.17 $ =~ /(\d+)\.(\d+)/);
  +our $VERSION = sprintf("%d.%02d", q$Revision: 1.18 $ =~ /(\d+)\.(\d+)/);
   our $DEBUG   = 0 unless defined $DEBUG;
   our $ERROR   = '';
   our $THROW   = 'parser';
  @@ -119,6 +119,38 @@
   our $ENDPAREN = qr/ \G \) /x;
   
   
  +#------------------------------------------------------------------------
  +# error messages throw by the parser
  +#------------------------------------------------------------------------
  +
  +our $ERRORS = {
  +    unterminated_string => 'unterminated string (missing %s)',
  +    text_after_embedded => 'unexpected text after embedded variable: %s',
  +    end_tag_after_binop => "unexpected end of directive tag after '%s'",
  +    no_static_variables => 'variables are not allowed in static paths',
  +    no_right_bracket    => 'no right bracket defined to match $1',
  +    internal_error      => 'internal error in %s, please notify the developers',
  +    bad_expr_after      => "unexpected %s after '%s' where expression expected",
  +    args_end_text       => 'unexpected end of text in argument list',
  +    in_list_def         => 'unexpected %s in list definition [ ... ]',
  +    in_hash_def         => 'unexpected %s in hash definition { ... }',
  +    in_parens           => 'unexpected %s in parentheses ( ... )',
  +    in_args             => 'unexpected %s in argument list ( ... )',
  +    no_colon            => "missing ':' after tertiary '%s' expression (%s)",
  +    no_dot_item         => "missing item after '.' (%s)",
  +    no_variable         => "missing variable after \$ (%s)",
  +    no_embedded         => "missing variable in \${ ... } (%s)",
  +    no_end_paren        => "missing ')' at end of argument list (%s)",
  +    missing_after       => "missing '%s' after '%s' (%s)",
  +    no_key_assign       => "missing assignment after key %s (%s)",
  +    no_var_assign       => "missing assignment after variable (%s)",
  +    no_expr_after       => "missing expression after '%s' (%s)",
  +    no_hash_assign      => "missing assignment after hash key %s (%s)",
  +    no_param_assign     => "missing assignment after parameter %s (%s)",
  +    no_ident_assign     => "missing assignment after identifier '%s' (%s)",
  +};
  +
  +
   
   #------------------------------------------------------------------------
   # init()
  @@ -218,7 +250,7 @@
   
           # check for end of tag
           if ($tag_end && ($$textref =~ /$tag_end/cg)) {
  -            return $self->error("unexpected end of directive tag after '$binop'")
  +            return $self->error_msg( end_tag_after_binop => $binop )
                   if @terms;
               $stop = 1;
               last CHUNK;
  @@ -235,8 +267,7 @@
                   $term = [ unary => $unop, $term ];
               }
               else {
  -                return $self->unexpected($textref, 
  -                       "after '$unop' where expression expected");
  +                return $self->unexpected($textref, bad_expr_after => $unop);
               }
           }
           elsif ($term = $self->parse_term($textref)) {
  @@ -247,8 +278,7 @@
                   # we haven't found a term, but we've already got some 
                   # terms push onto @terms, so this must be coming after
                   # a binary operator, which means it's a syntax error
  -                return $self->unexpected($textref, 
  -                       "after '$binop' where expression expected");
  +                return $self->unexpected($textref, bad_expr_after => $binop);
               }
               else {
                   $self->debug("no term\n") if $DEBUG;
  @@ -299,20 +329,18 @@
           $self->debug("expr tertiary operator: $op\n") if $DEBUG;
   
           $true = $self->parse_expression($textref)
  -            || return $self->unexpected($textref, 
  -                                        "after '$op' where expression expected");
  +            || return $self->unexpected($textref, bad_expr_after => $op);
           
           # NOTE: tertiary operator could be redefined to something other 
           # than ':' so this error message could be wrong, but we can't 
           # print out the value we're looking for because it's a regex
  -        return $self->missing($textref, "':' after expression following '$op'")
  +        return $self->missing($textref, no_colon => $op)
               unless $$textref =~ /$self->{ otherwise }/cg;
   
           $op = $1;
   
           $false = $self->parse_expression($textref)
  -            || return $self->unexpected($textref, 
  -                                        "after '$op' where expression expected");
  +            || return $self->unexpected($textref, bad_expr_after => $op);
   
           $expr = [ tertiary => $expr, $true, $false ];
       }
  @@ -361,7 +389,7 @@
       }
       elsif ($$textref =~ /$BADQUOTE/cog) {
           # check for unterminated strings - a common error
  -        return $self->error("unterminated string (missing $1)");
  +        return $self->error_msg( unterminated_string => $1 );
       }
       elsif ($$textref =~ /$QWLIST/cog) {
           $term = $self->parse_qwlist($textref, $1) || return;
  @@ -369,17 +397,17 @@
       }
       elsif ($$textref =~ /$LIST/cog) {
           $term = $self->parse_list($textref) || return;
  -        return $self->unexpected($textref, 'in list definition [ ... ]')
  +        return $self->unexpected($textref, 'in_list_def')
               unless $$textref =~ /$ENDLIST/cog;
       }
       elsif ($$textref =~ /$HASH/cog) {
           $term = $self->parse_hash($textref) || return;
  -        return $self->unexpected($textref, 'in hash definition { ... }')
  +        return $self->unexpected($textref, 'in_hash_def')
               unless $$textref =~ /$ENDHASH/cog;
       }
       elsif ($$textref =~ /$PAREN/cog) {
           $term = $self->parse_parens($textref) || return;
  -        return $self->unexpected($textref, 'in parentheses ( ... )')
  +        return $self->unexpected($textref, 'in_parens')
               unless $$textref =~ /$ENDPAREN/cog;
           $term = [ parens => $term ];
       }
  @@ -536,11 +564,11 @@
               # embedded ${ variable }
               my $text = $2;
               $token = $self->parse_variable(\$text)
  -                || return $self->missing($textref, "variable in '\${ }'");
  +                || return $self->missing($textref, 'no_embedded');
               if ($text =~ / \G \s* (.+) /cgsx) {
                   $text = $1;
                   $text =~ s/\n/\\n/g;
  -                return $self->error("unexpected text after embedded variable: $text");
  +                return $self->error_msg( text_after_embedded => $text );
               }
               push(@tokens, $token);
           }
  @@ -548,11 +576,12 @@
               # $variable reference
               my $text = $3;
               $token = $self->parse_variable(\$text) 
  -                || return $self->missing($textref, 'variable after \$');
  +                || return $self->missing($textref, 'no_variable');
               push(@tokens, $token);
           }
           else {
  -            return $self->error("no match in parse_string()");
  +            # this should never happen (but never say "never" :-)
  +            return $self->error_msg( internal_error => 'parse_string()' );
           }
       }
   
  @@ -561,7 +590,164 @@
   
   
   
  +#------------------------------------------------------------------------
  +# parse_key($textref)
  +#
  +# Parse anything that can be used as the key for a hash, parameter name,
  +# etc: unquoted identifier word, single or double quoted string.
  +#------------------------------------------------------------------------
  +
  +sub parse_key {
  +    my ($self, $textref) = @_;
  +
  +    $self->debug("parse_key(", $self->next_chunk($textref), ")\n")
  +        if $DEBUG;
  +
  +    # save current string position
  +    my $pos = pos $$textref;
  +
  +    # look for something that can be the LHS of an assignment
  +    if ($$textref =~ /$IDENT/cog) {
  +        $self->{ key_parsed } = "'$1'";
  +        if ($self->{ directives }->{ $1 }) {
  +            pos $$textref = $pos;
  +            return $self->decline("not a key (got keyword: $1)");
  +        }
  +        return [ ident => $1 ];
  +    }
  +    elsif ($$textref =~ /$SQUOTE/cog) {
  +        $self->{ key_parsed } = "'$1'";
  +        return [ squote => $1 ];
  +    }
  +    elsif ($$textref =~ /$DQUOTE/cog) {
  +        $self->{ key_parsed } = "\"$1\"";
  +        my $text = $1;
  +        my $string = $self->parse_string(\$text) || return;
  +        return [ dquote => $string ];
  +    }
  +
  +    pos $$textref = $pos;
  +    return $self->decline('not a key');
  +}
  +
  +
  +#------------------------------------------------------------------------
  +# parse_path($textref, %options)
  +#
  +# Parse a template path.  This can be an unquoted filename containing
  +# word characters plus a few others ('.' and '/' for example), a
  +# single quoted or double quoted string, or a variable reference
  +# explicitly prefixed with '$'.  The 'static' option can be set to 
  +# restrict the method to only parse static values: filenames, single
  +# quoted strings, and double quoted string that contain only text - 
  +# no double quoted or naked variables allowed!
  +#------------------------------------------------------------------------
  +
  +sub parse_path {
  +    my ($self, $textref, %opts) = @_;
  +    my $term;
  +
  +    $self->debug("parse_path('", $self->next_chunk($textref), "')\n")
  +        if $DEBUG;
  +
  +    my $pos = pos $$textref;
  +
  +    if ($$textref =~ /$FILENAME/cog) {
  +        # unquoted filename, but make sure it's not a directive keyword
  +        if ($self->{ directives }->{ $1 }) {
  +            pos $$textref = $pos;
  +            return $self->decline("not a path (got keyword: $1)");
  +        }
  +        $term = [ filename => $1 ];
  +    }
  +    elsif ($$textref =~ /$SQUOTE/cog) {
  +        $term = [ squote => $1 ];
  +    }
  +    elsif ($$textref =~ /$DQUOTE/cog) {
  +        my $text = $1;
  +        $term = $self->parse_string(\$text);
  +
  +        # if the 'static' option is set then check that the double quoted
  +        # string contains static text only, and no variable references
  +        if ($opts{ static }) {
  +            foreach (@$term) {
  +                return $self->error_msg('no_static_variables')
  +                    if ref $_;
  +            } 
  +        }
  +        $term = [ dquote => $term ];
  +    }
  +    elsif ($$textref =~ /$INTERP/cog) {
  +        # similarly, we only allow variable references if the static
  +        # option isn't set
  +        if ($opts{static}) {
  +            return $self->error_msg('no_static_variables');
  +        }
  +        else {
  +            $term = $self->parse_variable($textref) 
  +                || return $self->missing($textref, 'no_variable');
  +        }
  +    }
  +    else {
  +        return $self->decline('not a path');
  +    }
  +
  +    return $term;
  +}
  +
  +
  +#------------------------------------------------------------------------
  +# parse_static_path($textref)
  +#
  +# Wrapper around parse_path() which sets the 'static' option to 1.
  +#------------------------------------------------------------------------
  +
  +sub parse_static_path {
  +    my ($self, $textref) = @_;
  +    return $self->parse_path($textref, static => 1)
  +        || $self->decline('not a static path');
  +}
  +
  +
   
  +
  +sub parse_name {
  +    my $self = shift;
  +    my ($pkg, $file, $line) = caller(0);
  +    return $self->error("parse_name() has been changed to parse_statc_path()\n",
  +                        "please fix the code calling it at $file line $line\n");
  +}
  +
  +sub parse_template_name {
  +    my $self = shift;
  +    my ($pkg, $file, $line) = caller(0);
  +    return $self->error("parse_template_name() has been changed to parse_name()\n",
  +                        "please fix the code calling it at $file line $line\n");
  +}
  +
  +sub parse_filename {
  +    my ($self, $textref) = @_;
  +    my ($pkg, $file, $line) = caller(0);
  +    return $self->error("parse_filename() has been deprecated\n",
  +                        "please fix the code calling it at $file line $line\n");
  +}
  +
  +
  +
  +
  +#------------------------------------------------------------------------
  +# parse_paths()
  +#
  +# Parse one or more template paths separated by '+'.
  +#------------------------------------------------------------------------
  +
  +sub parse_paths {
  +    my $self = shift;
  +    return $self->error("parse_paths() not yet implemented");
  +}
  +
  +
  +
   #========================================================================
   # variables
   #========================================================================
  @@ -592,7 +778,7 @@
               $self->debug("found KEYWORD at variable position, declining\n") 
                   if $DEBUG;
               pos $$textref = $pos;
  -            return $self->decline("found keyword for next directive: $ident");
  +            return $self->decline("not a variable (got keyword: $ident)");
           }
   
           $term = [ ident => $ident ];
  @@ -602,7 +788,7 @@
               $args = $self->parse_args($textref) || return $args;
   
               # moved from parse_args()
  -            return $self->missing($textref, "')' at end of argument list")
  +            return $self->missing($textref, 'no_end_paren')
                   unless ($$textref =~ /$ENDPAREN/cog);
           }
       }
  @@ -617,7 +803,7 @@
       if ( $$textref =~ /$DOTOP/cog ) {
           # TODO: not sure why this is set to $term which is subsequently ignored...
           $self->parse_varnodes($textref, $terms)
  -            || return $self->missing($textref, "item after '.'");
  +            || return $self->missing($textref, 'no_dot_item');
       }
   
       return [ variable => $terms ];
  @@ -641,7 +827,7 @@
       do {
           ($node = $self->parse_varnode($textref))
               || return @$nodes 
  -                    ? $self->missing($textref, "item after '.'") 
  +                    ? $self->missing($textref, 'no_dot_item') 
                       : $node;
           push(@$nodes, $node);
       }
  @@ -694,22 +880,22 @@
       }
       elsif ($$textref =~ /$LIST/cog) {
           $term = $self->parse_list($textref) || return;
  -        return $self->unexpected($textref, 'in list definition [ ... ]')
  +        return $self->unexpected($textref, 'in_list_def')
               unless $$textref =~ /$ENDLIST/cog;
       }
   #    elsif ($$textref =~ /$HASH/cog) {
   #        $term = $self->parse_hash($textref) || return;
  -#        return $self->unexpected($textref, 'in hash definition { ... }')
  +#        return $self->unexpected($textref, 'in_hash_def')
   #            unless $$textref =~ /$ENDHASH/cog;
   #    }
       elsif ($$textref =~ /$EMBED/cog) {
           my $text = $1;
           ($term = $self->parse_variable(\$text))
  -            || return $self->missing($textref, "variable in '\${ }'");
  +            || return $self->missing($textref, 'no_embedded');
       }
       elsif ($$textref =~ /$INTERP/cog) {
           ($term = $self->parse_variable($textref))
  -            || return $self->missing($textref, "variable after '\$'");
  +            || return $self->missing($textref, 'no_variable');
       }
       else {
           return $self->decline('not a variable node');
  @@ -719,7 +905,7 @@
           $args = $self->parse_args($textref) || return;
   
           # moved from parse_args
  -        return $self->missing($textref, "')' at end of argument list")
  +        return $self->missing($textref, 'no_end_paren')
               unless ($$textref =~ /$ENDPAREN/cog);
       }
   
  @@ -758,8 +944,7 @@
                   push(@items, [ range => $item, $end ]);
               }
               else {
  -                return $self->unexpected($textref, 
  -                      "after '$range' where expression expected");
  +                return $self->unexpected($textref, bad_expr_after => $range);
               }
           }
           else {
  @@ -789,12 +974,11 @@
   
       # look up regex to match corresponding right bracket
       my $regex = $RBRACKET->{ $left }
  -        || return $self->error("no right bracket defined to match $1\n");
  -
  +        || return $self->error_msg( no_right_bracket => $1 );
       my $right = $BRACKETS->{ $left };
   
       # match text up to right bracket
  -    return $self->missing($textref, "$right after 'qw$left'")
  +    return $self->missing($textref, missing_after => $right, "qw$left")
           unless $$textref =~ /$regex/gc;
   
       return [ qwlist => $left, $1, $right ];
  @@ -820,8 +1004,7 @@
   
       while ($key = $self->parse_key($textref)) {
           $value = $self->parse_assign_expr($textref)
  -            || return $self->missing($textref, 
  -                                     "assignment after hash key $self->{ key }");
  +            || return $self->missing($textref, no_hash_assign => $self->{ key_parsed });
           push(@hash, [ $key, $value ]);
               
           # skip comma and/or whitespace
  @@ -836,47 +1019,6 @@
   
   
   #------------------------------------------------------------------------
  -# parse_key($textref)
  -#
  -# Parse anything that can be used as the key for a hash, parameter name,
  -# etc: unquoted identifier word, single or double quoted string.
  -#------------------------------------------------------------------------
  -
  -sub parse_key {
  -    my ($self, $textref) = @_;
  -
  -    $self->debug("parse_key(", $self->next_chunk($textref), ")\n")
  -        if $DEBUG;
  -
  -    # save current string position
  -    my $pos = pos $$textref;
  -
  -    # look for something that can be the LHS of an assignment
  -    if ($$textref =~ /$IDENT/cog) {
  -        $self->{ key } = "'$1'";
  -        if ($self->{ directives }->{ $1 }) {
  -            pos $$textref = $pos;
  -            return $self->decline("found directive keyword: $1");
  -        }
  -        return [ ident => $1 ];
  -    }
  -    elsif ($$textref =~ /$SQUOTE/cog) {
  -        $self->{ key } = "'$1'";
  -        return [ squote => $1 ];
  -    }
  -    elsif ($$textref =~ /$DQUOTE/cog) {
  -        $self->{ key } = "\"$1\"";
  -        my $text = $1;
  -        my $string = $self->parse_string(\$text) || return;
  -        return [ dquote => $string ];
  -    }
  -
  -    pos $$textref = $pos;
  -    return $self->decline("not a key");
  -}
  -
  -
  -#------------------------------------------------------------------------
   # parse_args($textref)
   #
   # Parse the contents of a parenthesised argument list.
  @@ -953,8 +1095,7 @@
   
       while ($key = $self->parse_key($textref)) {
           $value = $self->parse_assign_expr($textref)
  -            || return $self->missing($textref, 
  -                                     "assignment after parameter $self->{ key }");
  +            || return $self->missing($textref, no_param_assign => $self->{ key_parsed });
           push(@$params, [ tuple => $key, $value ]);
   
           # skip comma and/or whitespace
  @@ -968,8 +1109,6 @@
   }
   
   
  -    
  -
   #------------------------------------------------------------------------
   # parse_parens($textref)
   # 
  @@ -993,44 +1132,10 @@
   }
   
   
  -#------------------------------------------------------------------------
  -# parse_assign($textref)
  -#
  -# Parses an assignement statement of the form: variable =>? expression
  -#------------------------------------------------------------------------
  -
  -sub parse_assign {
  -    my ($self, $textref) = @_;
  -    my ($var, $op, $value, $assign);
  -
  -    $self->debug("parse_assign()\n") if $DEBUG;
   
  -    # skip any leading whitespace
  -    $$textref =~ /$self->{ wspace }/cg;
  -
  -    # save string position in case we need to backtrack
  -    my $pos = pos $$textref;
  -
  -    # look for a variable
  -    if ($var = $self->parse_variable($textref)) {
  -        if ($$textref =~ /$self->{ assign }/cg) {
  -            $op = $1;
  -            $self->debug(" - assign ($op)\n") if $DEBUG;
  -            
  -            $value = $self->parse_expression($textref)
  -                || return $self->unexpected($textref, 
  -                                 "after '$op' where expression expected");
  -            return [ assign => $var, $value ];
  -        }
  -    }
  -
  -    # rewind string position to start of variable
  -    $self->debug("- not assign, backtracking\n") if $DEBUG;
  -
  -    pos $$textref = $pos;
  -
  -    return $self->decline('not an assignment');
  -}
  +#========================================================================
  +# assignments
  +#========================================================================
   
   
   #------------------------------------------------------------------------
  @@ -1050,9 +1155,9 @@
       $$textref =~ /$self->{ wspace }/cg;
   
       if ($$textref =~ /$self->{ assign }/cg) {
  -        $op = $1;
  +        $op = $self->{ assign_parsed } = $1;
           return $self->parse_expression($textref)
  -            || $self->missing($textref, "expression after '$op'");
  +            || $self->missing($textref, no_expr_after => $op);
       }
       else {
           return $self->decline('not an assignment');
  @@ -1068,22 +1173,104 @@
   #------------------------------------------------------------------------
   
   sub parse_ident_assign_expr {
  -    my ($self, $textref) = @_;
  -    my ($ident, $expr);
  +    my ($self, $textref, %opts) = @_;
   
       $self->debug("parse_ident_assign_expr(", 
                    $self->next_chunk($textref), ")") if $DEBUG;
   
       $$textref =~ /$self->{ wspace }/cgx;
  +
  +    my $pos   = pos $$textref;
  +    my $ident = $self->parse_ident($textref) || return;
  +
  +    if (my $expr = $self->parse_assign_expr($textref, %opts)) {
  +        return [ $ident, $expr ];
  +    }
  +    elsif ($opts{ required }) {
  +        return $self->missing($textref, no_ident_assign => $ident);
  +    }
  +    else {
  +        pos $$textref = $pos;
  +        return $self->decline('not an assignment');
  +    }
  +}
  +
  +
  +#------------------------------------------------------------------------
  +# parse_key_assign_expr($textref)
  +#
  +# Parses an assignment of a hash key (ident, single or double quoted) to 
  +# an expression, e.g. "x = 10", "y => x", "z = x || y".
  +#------------------------------------------------------------------------
  +
  +sub parse_key_assign_expr {
  +    my ($self, $textref, %opts) = @_;
  +
  +    $self->debug("parse_key_assign_expr(", 
  +                 $self->next_chunk($textref), ")") if $DEBUG;
  +
  +    $$textref =~ /$self->{ wspace }/cgx;
  +
  +    my $pos = pos $$textref;
  +    my $key = $self->parse_key($textref) || return;
   
  -    $ident = $self->parse_ident($textref) || return;
  -    $expr  = $self->parse_assign_expr($textref)
  -        || return $self->missing($textref, "assignment after identifier '$ident'");
  -    return [ $ident, $expr ];
  +    if (my $expr = $self->parse_assign_expr($textref)) {
  +        return [ $key, $expr ];
  +    }
  +    elsif ($opts{ required }) {
  +        return $self->missing($textref, no_key_assign => $self->{ key_parsed });
  +    }
  +    else {
  +        pos $$textref = $pos;
  +        return $self->decline('not an assignment');
  +    }
   }
   
   
   #------------------------------------------------------------------------
  +# parse_var_assign_expr($textref)
  +#
  +# Parses an assignement statement of the form: variable =>? expression
  +#------------------------------------------------------------------------
  +
  +sub parse_var_assign_expr {
  +    my ($self, $textref, %opts) = @_;
  +
  +    $self->debug("parse_var_assign_expr()\n") if $DEBUG;
  +
  +    $$textref =~ /$self->{ wspace }/cg;
  +
  +    my $pos = pos $$textref;
  +    my $var = $self->parse_variable($textref) || return;
  +
  +    if (my $expr = $self->parse_assign_expr($textref)) {
  +        return [ $var, $expr ];
  +    }
  +    elsif ($opts{ required }) {
  +        return $self->missing($textref, 'no_var_assign');
  +    }
  +    else {
  +        pos $$textref = $pos;
  +        return $self->decline('not an assignment');
  +    }
  +}
  +
  +
  +
  +sub parse_assign {
  +    my $self = shift;
  +    my $expr = $self->parse_var_assign_expr(@_) || return;
  +    unshift(@$expr, 'assign');
  +    return $expr;
  +    my ($pkg, $file, $line) = caller(0);
  +    return $self->error("deprecated parse_assign() method called at $file line $line\n");
  +}
  +
  +
  +
  +
  +
  +#------------------------------------------------------------------------
   # parse_ident_args($text)
   #
   # Parser a simple identifier (e.g. variable name) optionally followed by
  @@ -1097,23 +1284,11 @@
   
       $self->debug("parse_ident_args(", $self->next_chunk($textref), ")\n")
           if $DEBUG;
  -
  -    my $pos = pos $$textref;
  -
  -    # look for an identifier
  -    return $self->decline('no identifier')
  -        unless $$textref =~ /$IDENT/cog;
   
  -    my $ident = $1;
  +    my $pos   = pos $$textref;
  +    my $ident = $self->parse_ident($textref) || return;
  +    my $term  = [ ident => $ident ];
   
  -    if ($self->{ directives }->{ $ident }) {
  -        $self->debug("found directive keyword, declining\n") if $DEBUG;
  -        pos $$textref = $pos;
  -        return $self->decline("no identifier, got directive keyword: $ident");
  -    }
  -
  -    my $term = [ ident => $ident ];
  -
       # args may follow
       if ($$textref =~ /$PAREN/cog) {
           my @args;
  @@ -1129,13 +1304,13 @@
                   # then end of the tag, then report a missing ')', otherwise we
                   # report that a non-identifier was found in the args list
                   if ($$textref =~ / (?= \G \s* $ ) /cgsx) {
  -                    return $self->error('unexpected end of text in argument list');
  +                    return $self->error_msg('args_end_text');
                   }
                   elsif ($tag_end && ($$textref =~ /$tag_end/cg)) {
  -                    return $self->missing($textref, "')' at end of argument list");
  +                    return $self->missing($textref, 'no_end_paren');
                   }
                   else {
  -                    return $self->unexpected($textref, 'in argument list');
  +                    return $self->unexpected($textref, 'in_args');
                   }
               }
   
  @@ -1153,94 +1328,6 @@
   }
   
   
  -
  -
  -#------------------------------------------------------------------------
  -# parse_filename($textref)
  -#
  -# filename: FILENAME
  -#         | QUOTED
  -#         | LITERAL
  -#------------------------------------------------------------------------
  -
  -sub parse_filename {
  -    my ($self, $textref) = @_;
  -    my $term;
  -
  -    $self->debug("parse_filename('", $self->next_chunk($textref), "')\n")
  -        if $DEBUG;
  -
  -    my $pos = pos $$textref;
  -
  -    if ($$textref =~ /$FILENAME/cog) {
  -        if ($self->{ directives }->{ $1 }) {
  -            $self->debug("found directive keyword, declining\n") if $DEBUG;
  -            pos $$textref = $pos;
  -            return $self->decline("no filename, got directive keyword: $1");
  -        }
  -        $term = [ filename => $1 ];
  -    }
  -    elsif ($$textref =~ /$SQUOTE/cog) {
  -        $term = [ squote => $1 ];
  -    }
  -    elsif ($$textref =~ /$DQUOTE/cog) {
  -        # we'll accept double quote strings, but only if they contain static
  -        # text and don't have any embedded variables in them.  
  -        my $text = $1;
  -        $term = $self->parse_string(\$text);
  -        foreach (@$term) {
  -            return $self->error('invalid variables in double quoted file name')
  -                if ref $_;
  -        } 
  -        $term = [ dquote => join('', @$term) ];
  -    }
  -    else {
  -        return $self->decline('no file name');
  -    }
  -
  -    return $term;
  -}
  -
  -
  -
  -#------------------------------------------------------------------------
  -# parse_template_name($textref)
  -#
  -# filename: FILENAME
  -#         | QUOTED
  -#         | LITERAL
  -#         | $variable
  -#------------------------------------------------------------------------
  -
  -sub parse_template_name {
  -    my ($self, $textref) = @_;
  -    my $term;
  -
  -    if ($$textref =~ /$FILENAME/cog) {
  -        $term = [ filename => $1 ];
  -    }
  -    elsif ($$textref =~ /$SQUOTE/cog) {
  -        $term = [ squote => $1 ];
  -    }
  -    elsif ($$textref =~ /$DQUOTE/cog) {
  -        my $text = $1;
  -        $term = $self->parse_string(\$text);
  -        $term = [ dquote => $term ];
  -    }
  -    elsif ($$textref =~ /$INTERP/cog) {
  -        $self->debug("parsing filename variable\n") if $DEBUG;
  -        $term = $self->parse_variable($textref) 
  -            || return $self->missing($textref, "variable after '\$'");
  -    }
  -    else {
  -        return $self->decline('not a template name');
  -    }
  -
  -    return $term;
  -}
  -
  -
  -
   #------------------------------------------------------------------------
   # parse_whitespace($text)
   #
  @@ -1321,21 +1408,21 @@
   
   
   #------------------------------------------------------------------------
  -# unexpected(\$text, $message)
  +# unexpected(\$text, $code, @args)
   #
   # Error reporting method.
   #------------------------------------------------------------------------
   
   sub unexpected {
  -    my ($self, $textref, @message) = @_;
  +    my ($self, $textref, $code, @args) = @_;
       my $next = $self->next_token($textref);
       $next = length $next ? "'$next'" : 'end of statement';
  -    return $self->error("unexpected $next ", @message); 
  +    return $self->error_msg($code, $next, @args); 
   }
   
   
   #------------------------------------------------------------------------
  -# missing(\$text, $message)
  +# missing(\$text, $code, @args)
   #
   # Error checking and reporting method.  If $value is defined (but 
   # usually false) then it indicates that the parser has declined to parse
  @@ -1345,10 +1432,10 @@
   #------------------------------------------------------------------------
   
   sub missing {
  -    my ($self, $textref, @error) = @_;
  +    my ($self, $textref, $code, @args) = @_;
       my $next = $self->next_token($textref);
  -    $next = defined $next && length $next ? " (got '$next')" : '';
  -    return $self->error('missing ', @error, $next);
  +    $next = defined $next && length $next ? "got '$next'" : 'end of statement';
  +    return $self->error_msg($code, @args, $next);
   }
   
   
  @@ -2063,9 +2150,58 @@
   
   Parses the contents of a double quoted string.  This does most of the 
   dirty work for parse_dquote().
  +
  +=head2 parse_key(\$text)
   
  -=head2 parse_name(\$text)
  +This method parses a hash key.  This can be an unquoted identifier, a
  +single or double quoted string.
   
  +For example:
  +
  +    foo
  +    'foo'
  +    "foo"
  +    "foo$bar"
  +    "${foo}bar"
  +
  +=head2 parse_path(\$text, %options)
  +
  +This method parses a template path containing any combination of
  +alphanumeric characters, C<_>, C</>, C<.> or C<:>.
  +
  +For example:
  +
  +    header
  +    my_header
  +    my/header
  +    my/header.html
  +    file:my/header.html
  +    http://example.com/templates/header.html
  +    site_header
  +    index.html
  +
  +It can also be expressed as a single or double quoted string.  Double
  +quoted strings can contain embedded variables (but see the C<static>
  +option described below).
  +
  +    'header'
  +    "header"
  +    "header.$ext"
  +    "${filename}.$ext"
  +
  +The path can also be specified by reference to a variable.  The variable
  +should be prefixed with C<$>, just as you would when embedding a variable
  +in a double quoted string.  
  +
  +    $filename
  +    $my.templates.header
  +
  +The C<static> option can be set to indicate that the method should only
  +accept static paths that don't contain any variable references, either 
  +
  +    
  +        
  +    
   TODO: this method needs work.  should handle name+name+name and other
   stuff.
   
  @@ -2079,6 +2215,8 @@
       "my$file"
       $filename
   
  +=head2 parse_static_path(\$text)
  +
   TODO: more docs on this
   
   =head2 parse_variable(\$text)
  @@ -2215,11 +2353,6 @@
         ]
       ]
   
  -=head2 parse_key(\$text)
  -
  -TODO: method for parsing a hash key.  can be an unquoted identifier, a
  -single or double quoted string, of variable node and any arguments
  -
   =head2 parse_args(\$text)
   
   TODO: parse a list of arguments, as enclosed by parens.  Only parses the 
  @@ -2240,14 +2373,6 @@
   
   TODO: Parse an assignment or expression enclosed in parentheses.
   
  -=head2 parse_assign(\$text)
  -
  -TODO: parse an assignment of variable to expression, such as in the SET
  -directive.
  -
  -    foo = bar
  -    wiz.waz = a < b ? c : d
  -
   =head2 parse_assign_expr(\$text)
   
   TODO: parse an assignment to an expression
  @@ -2263,6 +2388,15 @@
       a = 10
       b = 20
   
  +=head2 parse_assign(\$text)
  +
  +TODO: parse an assignment of variable to expression, such as in the SET
  +directive.
  +
  +    foo = bar
  +    wiz.waz = a < b ? c : d
  +
  +
   =head2 parse_ident_args(\$text)
   
   TODO: parse an identifer followed by an optional argument list
  @@ -2351,9 +2485,16 @@
   When called without any arguments, the method returns the error message
   most recently set by a call to error() (with arguments, of course) or
   decline().
  +
  +=head2 error_msg($code, @args)
  +
  +TODO: this is used instead of error() to allow us to localise error 
  +messages, and simplify error handling in general.
   
  -=head2 missing(\$text, $thing)
  +=head2 missing(\$text, $code, @args)
   
  +TODO: this has changed - now a wrapper around error_msg()
  +
   This is a wrapper around the error() method which constructs an error
   message of the form "missing $thing (got 'blah')".  The first argument
   is a reference to the current input text string.  The method uses this
  @@ -2382,6 +2523,8 @@
   
   =head2 unexpected(\$text, $message)
   
  +TODO: this has changed - now a wrapper around error_msg()
  +
   Like the missing() method, this provides a wrapper around the error()
   method.
   
  @@ -2433,7 +2576,7 @@
   
   =head1 VERSION
   
  -$Revision: 1.17 $
  +$Revision: 1.18 $
   
   =head1 COPYRIGHT