"""quixote.template

Template compiler code.
"""

__revision__ = "$Id: template.py,v 1.12 2000/05/31 22:11:12 akuchlin Exp $"

import string, types, re
import pprint

# Define constants for directives
constants = ['EVAL', 'STMT', 'BLOCK', 'ENDBLOCK', 
             'WHILE', 'ENDWHILE', 'BREAK', 'CONTINUE',
             'IF', 'ELIF', 'ELSE', 'ENDIF', 
	     'PRINT', 'RAISE', 'TRY', 'ENDTRY', 'EXCEPT',
             'IMPORT', 'FOR', 'ENDFOR', 'TEMPLATE', 'ENDTEMPLATE' ]
directive_integer = {}
directive_string  = {}

num = 1
for c in constants: 
    exec c + '=' + str(num)
    directive_string[ num ] = c
    directive_integer[ c ]  = num
    num = num + 1 

# Maps keyword to the corresponding directive constant
keywords = {'stmt':STMT, 'block':BLOCK, '/block':ENDBLOCK,
            'if':IF, 'elif':ELIF, 'else':ELSE, '/if':ENDIF,
            'while':WHILE, '/while': ENDWHILE,
            'for': FOR, '/for':ENDFOR,
            'template': TEMPLATE, '/template': ENDTEMPLATE,
            'print' : PRINT,
         }

class ParseError:
    """Syntax or semantic error while parsing a template file.
    """

# Number of spaces to use in indenting the generated Python code
# for a template.
INDENT_INCREMENT = 2

class Template:
    """Class representing a compiled PTL template.

    Attributes:
    tree -- data structure generated by parsing a template
    name -- name of template (from signature); None if not specified
    signature -- parameters from signature; None if not specified
    __file__ -- file from which this template came
    
    Methods:
    __call__( [namespace] ) -- evaluate the template using the provided
    dictionary-like object as the namespace.
    """

    def __init__ (self):
        self.name = self.signature = self.__file__ = None
        self.func = None
        
        actions = self.actions = {}
        for name, integer in directive_integer.items():
            meth_name = 'action_' + name
            if hasattr(self, meth_name):
                actions[ integer ] = getattr(self, meth_name)

    signature_pat = re.compile("""\s*
                                  (\w+) # Function name
                                  \s*
                                  [(] (.*) [)] # Function parameters
                                  \s*$  # Rest of line
    """, re.VERBOSE | re.DOTALL)
    
    def set_signature (self, signature):
        """set_signature(signature:string)

        Parse 'signature', which is a function name and signature
        such as "foo(arg1, arg2, arg3=None)", and sets the 'name'
        and 'signature' attributes.
        """
        
        m = self.signature_pat.match(signature)
        if m is None:
            raise ParseError, "Can't parse signature: " + repr(signature)
        self.name, self.signature = m.group(1,2)

    def set_tree (self, tree):
        """set_tree(tree: tuple) -> None

        Specify 'tree' as the parse tree for this template.  Parse
        trees are simply tuples, containing either strings for literal
        text, subtrees also represented as tuples. The first element
        of a subtree is a constant such as EVAL, STMT, or whatever;
        the remaining elements are the children of the subtree.
        """
        
        self.tree = tree

    def set_file (self, file):
        "Set the source filename of the template"
        self.__file__ = file

    def output_code (self):

        """Returns a string containing the generated Python code for
        this template.  set_tree() and set_signature() must have been
        previously called to specify the parse tree and the signature
        of the generated Python function.  """
        
        if self.tree is None:
            raise ValueError, "No syntax tree has been set"
        if self.name is None:
            raise ValueError, "No template name has been set"
        if self.signature is None:
            raise ValueError, "No template signature has been set"

        text = "def %s(%s):\n" % (self.name, self.signature)
        text = text + '  __output = ""\n'
        text = text + self.__generate_from_tree( self.tree, 2)
        text = text + '  return __output\n'

        return text


    # Private code generation methods
    def __generate_from_tree (self, tree, indent_level):
        "Generate Python code from the given parse tree"
        text = []
        for child in tree:
            if type(child) == types.StringType:
                text.append( indent_level*' ' +
                             '__output = __output + ' + repr(child) +
                             '\n' )
            elif type(child) == types.TupleType:
                action = child[0]
                meth = 'generate_' + directive_string[ action ]
                meth = getattr(self, meth)
                text.append( meth(child[1:], indent_level) )
            else:
                raise RuntimeError, "Unexpected syntax tree element: " +repr(child)

        return string.join(text, "")

    def generate_EVAL (self, args, indent_level):
        assert len(args) == 1
        return "%s__output = __output + str(%s)\n" % (indent_level*' ',
                                                      args[0])

    def generate_STMT (self, args, indent_level):
        assert len(args) == 1
        return "%s%s\n" % (indent_level*' ', args[0])

    def generate_WHILE (self, args, indent_level):
        assert len(args) == 2
        condition, while_block = args
        text = ("%swhile (%s):\n%s" % (indent_level*' ', condition,
                self.__generate_from_tree(while_block, indent_level +INDENT_INCREMENT ) ) )
        return text

    def generate_BLOCK (self, args, indent_level):
        assert len(args) == 1
        indent = indent_level * ' '
        block = indent + string.replace(args[0], '\n', '\n'+indent)
        block = string.rstrip(block) + '\n'
        return block

    def generate_IF (self, args, indent_level):
        # The arguments for 'if' are a list containing (condition, block)
        # 2-tuples, and a final single argument that is either the else
        # block or None.
        assert len(args) > 1
        first = 1 ; text = ""
        for condition, block in args[:-1]:
            if first: keyword = 'if'
            else: keyword = 'elif'

            text = text + '%s%s (%s):\n%s' % (indent_level*' ', keyword, condition,
                self.__generate_from_tree(block, indent_level +INDENT_INCREMENT ) )
            first = 0
                            
        else_block = args[-1]
        if else_block is not None:
            text = text + '%selse:\n%s' % (indent_level*' ',
               self.__generate_from_tree(else_block, indent_level +INDENT_INCREMENT ) )

        return text                                  

    def generate_FOR (self, args, indent_level):
        # The arguments to 'for' are (expr, sequence, block)
        assert len(args) == 3
        expr, sequence_expr, block = args

        text = "%sfor %s in %s:\n%s" % (indent_level*' ', expr, sequence_expr,
               self.__generate_from_tree(block, indent_level +INDENT_INCREMENT ) ) 
        return text

    def generate_PRINT (self, args, indent_level):
        # The arguments for 'print' are an arbitrary number of expressions,
        # that are output separated by spaces, and terminated by a newline
        assert len(args) > 0
        text = indent_level*' ' + "print "
        for expr in args[:-1]:
            text = text + expr + ' , '
        for expr in args[-1:]:
            text = text + expr + '\n'
        return text
    
            
                   
pi_pat = re.compile(r"""<\?             # Opening symbol
\s* (?:ptl\s)?                          # Optional 'ptl' prefix
\s* (.*?) \s*                           # Directive body
\?>                                     # Closing symbol
""", re.VERBOSE | re.DOTALL)

class Directive:
    """Class representing a single directive from the input stream

    Attributes:
    type -- constant (IF, BLOCK, etc.)
    value -- associated value
    start_line, start_col -- line and column numbers of starting point
    end_line, end_col -- line and column numbers of end point
    """

    def __init__ (self, value):
        m = re.match(r'(?s)^\s*(/?\w+)(?:\s+(.*))?', value)
        if m:
            first_word, rest = m.group(1, 2)
            if keywords.has_key(first_word):
                self.value = rest ; self.type = keywords[first_word]

                # Parse special forms
                if first_word == 'for':
                    m = re.match('^(.*?)\s+in\s+(.*)$', self.value)
                    if m is None:
                        raise ParseError, ("'for' statement incomprehensible (no 'in'?): "
                                           + repr(self.value) )
                    else:
                        self.value = m.group(1,2)
            else:
                self.type = EVAL
                self.value = value
        else:
            self.type = EVAL
            self.value = value
            
    def __repr__ (self):
        name = directive_string[ self.type ]
        return '<Directive %s value=%s>' % (name, self.value)

    def text_pos (self):
        "Return string given position in file"
        return "line %i, column %i" % (self.start_line, self.start_col)
    
def find_position (S, pos):
    "Return the line and column number of 'pos' in the string 'S'"
    line = 1 + string.count( S, '\n', 0, pos )

    prev_newline = string.rfind( S, '\n', 0, pos)
    if prev_newline == -1: column = pos + 1
    else: column = pos - prev_newline + 1

    return line, column

class Parser:
    def __init__ (self, directive_list):
	self.directive_list = directive_list
	self.pos = self.next_pos = 0
        actions = self.actions = {}
        for name, integer in directive_integer.items():
            meth_name = 'parse_' + name
            if hasattr(self, meth_name):
                actions[ integer ] = getattr(self, meth_name)

    def parse (self, depth=0):
	tree = []

        while 1:
            tok = self.get_next_directive()
##            print 'get_next_directive, depth = %i, return on %s' % (depth, tok)

            # If the directive returned is None, there are no more directives;
            # add the rest of the directive list (presumably just strings)
            # to the tree and return it.
            if tok is None:
                if depth != 0:
                    raise ParseError, 'Missing closing thing'
                else:
                    tree.extend( self.directive_list[self.pos:] )
                    return tuple(tree)

            # Exit for directives which mark the end of a block; collect up
            # the remaining strings and return the resulting tree
            elif tok.type in [ELIF, ELSE, EXCEPT,
                              ENDTRY, ENDIF, ENDBLOCK, ENDFOR,
                              ENDTEMPLATE, ENDWHILE]: 
                if depth == 0:
                    raise ParseError, ('extra closing directive at %s' %
                                       tok.text_pos() )
                else:
                    tree.extend( self.directive_list[self.pos:self.next_pos] )
                    return tuple(tree)

            tree.extend( self.directive_list[self.pos:self.next_pos] )
            self.consume_directives()

            if self.actions.has_key( tok.type ):
                meth = self.actions[ tok.type ]
                tree.append( meth( tok,depth ) )
            else:
                raise ParseError, "Unexpected directive type: " +repr(tok.type)
            
            # end while

    def parse_EVAL (self, tok, depth):
        return (EVAL, tok.value)

    def parse_STMT (self, tok, depth):
        return (STMT, tok.value)

    def parse_IF (self, tok, depth):
        block = self.parse( depth+1 )
        cond_list = [ (tok.value, block) ]

        tok = self.get_next_directive()
        while tok.type == ELIF:
            self.consume_directives()
            block = self.parse( depth+1 )
            cond_list.append( (tok.value, block) )
            tok = self.get_next_directive()

        if tok.type == ELSE:
            self.consume_directives()
            block = self.parse( depth+1 )
            cond_list.append( block )
            tok = self.get_next_directive()
        else:
            cond_list.append( None )

        if tok.type != ENDIF:
            raise ParseError, 'No closing /if'
        else:
            self.consume_directives()
            cond_list.insert( 0, IF )
            return tuple( cond_list ) 

    def parse_FOR (self, tok, depth):
        item, sequence = tok.value
        block = self.parse( depth+1 )
        tok = self.get_next_directive()
        if tok.type != ENDFOR:
            raise ParseError, 'No closing /for'
        else:
            self.consume_directives()
            return (FOR, item, sequence, block)

    def parse_TEMPLATE (self, tok, depth):
        if depth != 0 :
            raise ParseError, 'nested template directive encountered'

        signature = tok.value
        block = self.parse( depth+1 )
        tok = self.get_next_directive()
        if tok is None or tok.type != ENDTEMPLATE:
            raise ParseError, 'No closing /endtemplate'
        else:
            self.consume_directives()
            return (TEMPLATE, signature, block)

    def parse_WHILE (self, tok, depth):
        condition = tok.value
        block = self.parse( depth+1 )
        tok = self.get_next_directive()
        if tok.type != ENDWHILE:
            raise ParseError, 'No closing /while'
        else:
            self.consume_directives()
            return (WHILE, condition, block)

    def parse_BLOCK (self, tok, depth):
        tok = self.get_next_directive()
        if tok.type != ENDBLOCK:
            raise ParseError, 'No closing /block'
        else:
            blocktext = string.join( self.directive_list[self.pos:self.next_pos], '' )
            self.consume_directives()
            return (BLOCK, blocktext)
            
    def parse_PRINT (self, tok, depth):
        return (PRINT, tok.value)

    def get_next_directive (self):
	pos = self.pos 
	while (pos < len(self.directive_list) and 
	       not isinstance(self.directive_list[pos], Directive) ):
            pos = pos + 1
	    
	self.next_pos = pos 
	if pos < len(self.directive_list): return self.directive_list[pos]
	return None

    def consume_directives (self):
	self.pos = self.next_pos + 1
    
def compile_template (S):
    """Compile the string 'S', returning a list of Template instances
    found within the string.  This should always return a list of at least
    length 1.
    """
    
    # First pass: turn the string into a list of strings (literal HTML
    # text) and directives (for the processing instructions).    
    L = []
    pos = 0
    while 1:
        m = pi_pat.search(S, pos)
        if m is None: break

        L.append( S[ pos:m.start() ] )

        tok = Directive( m.group(1) )

        # Find starting and ending positions of directive
        tok.start_line, tok.start_col = find_position(S, m.start())
        tok.end_line, tok.end_col = find_position(S, m.end())
        
        L.append( tok )
        pos = m.end()

    L.append( S[pos:] )

    # Pass 2: compile the list of directives and strings into a parse
    # tree.
    p = Parser( L )
    tree = p.parse()

    # Pass 3: convert the parse tree to a list of templates

    # Check that either there are no template directives, or, if there
    # are template directives, the only sibling chunks of text are all
    # whitespace.
    template_seen = nontemplate_found = 0
    for child in tree:
        if (type(child) == types.StringType and
            string.strip(child) != ""):
            nontemplate_found = 1
        elif (type(child) == types.TupleType):
            if child[0] == TEMPLATE:
                template_seen = 1
            else:
                nontemplate_found = 1

    if nontemplate_found and template_seen:
        raise ParseError, "Templates mixed with non-whitespace characters"

    if template_seen:
        # Drop everything except the subtrees representing each template
        tree = filter(lambda x: type(x) == types.TupleType, tree)

        # Convert all the children into Template instances
        result = []
        for child in tree:
            assert child[0] == TEMPLATE
            t = Template()
            t.set_signature( child[1] )
            t.set_tree( child[2] )
            result.append( t )

        return result
    
    else:
        # Turn the entire tree into a single template,
        # and return it in a list of length 1.
        template = Template()
        template.set_tree( tree )
        return [template ]

if __name__ == '__main__':
    obj = compile_template("""<?template f(signature)?>
<?for i in range(50)?>
test
<?/for?>
<?/template?>
<?template f(cond, c1, c2=2.0)?>
<?/template?>
""")
    obj = compile_template("""<?if cond == 1?>c1
<?elif cond == 2?>c2
<?else?>else clause<?/if?>
<?block?>
for i in range(50):
    print i
<?/block?>
<?for i in range(50)?>
  test <? i ?>   <?while i<10?> low <?stmt i=i+1?> <?/while?>
<?/for?>
<?print c1, c2?>
""")
    obj[0].signature = 'cond, c1, c2=2.0'
    obj[0].name = 'f1'
    obj[0].set_file('dummy file')
    text = obj[0].output_code()
    exec text
    print f1(1, 3.5)
