#
# Copyright 2001 by Object Craft P/L, Melbourne, Australia.
#
# LICENCE - see LICENCE file distributed with this software for details.
#
# Templating core functionality
#
import re

from albatross.common import *

# The template file parser treats HTML files as plain text with
# embedded template tags.  In parsing the file a tag tree is
# constructed.  Tags are either empty or enclosing.


# Plain text from the template file is managed by a Text object.  This
# allows the template interpreter to treat text no differently from a
# tag.

class Text:

    name = None

    def __init__(self, text):
        self.text = text

    def __repr__(self):
        return 'Text(%s)' % self.text

    def to_html(self, ctx):
        ctx.write_content(self.text)


# All tags in the toolkit and application are implemented by
# subclassing either EmptyTag or EnclosingTag.  Each of these classes
# inherit from Tag.

class Tag:

    '''Functionality common to all HTML template tags.
    '''

    def __init__(self, ctx, filename, line_num, attribs):
        '''Store where the tag was defined for execution trace
        generation
        '''
        self.filename = filename
        self.line_num = line_num
        self.attribs = attribs
        self.attrib_order = None

    def __repr__(self):
        parts = []
        parts.append('<')
        parts.append(self.name)
        for name in self.attrib_order:
            value = self.attribs[name]
            if value is not None:
                parts.append(' %s="%s"' % (name, value))
            else:
                parts.append(' %s' % name)
        parts.append('>')
        return ''.join(parts)

    def raise_error(self, msg):
        raise ApplicationError('%s:%s: %s' % \
                               (self.filename, self.line_num, msg))

    def has_attrib(self, name):
        '''Was the named attribute named with the tag
        '''
        return self.attribs.has_key(name)

    def assert_has_attrib(self, name):
        if not self.attribs.has_key(name):
            self.raise_error('missing "%s" attribute' % name)

    def get_attrib(self, name, default = None):
        '''Return the value of the named attribute
        '''
        return self.attribs.get(name, default)

    def set_attrib_order(self, order):
        self.attrib_order = order

    def set_attrib(self, name, value):
        '''Set the value of the named attribute
        '''
        self.attribs[name] = value
        if name not in self.attrib_order:
            self.attrib_order.append(name)

    def attrib_items(self):
        return self.attribs.items()

    def write_attribs_except(self, ctx, *exclude):
        for name in self.attrib_order:
            if name in exclude:
                continue
            value = self.attribs[name]
            if value is not None:
                ctx.write_content(' %s="%s"' % (name, value))
            else:
                ctx.write_content(' %s' % name)


# Tags such as <al-input>, <al-let>, <al-usearg> do not enclose
# content.  These tags just use the information in their attributes to
# generate output.
# 
# When you subclass the EmptyTag class you must supply a name class
# attribute and you should almost certainly override the to_html()
# method.
# 
# class HR(template.EmptyTag):
#     name = 'al-hr'
#
#     def to_html(self, ctx):
#         ctx.write_content('<hr noshade>')
#
# The name attribute allows the code which interprets the parsed
# template to identify each tag.  It is also used by the parser to
# locate which class to instantiate when it finds a tag of the same
# name in the template file.

class EmptyTag(Tag):

    '''All tags which do not enclose content inherit from this class
    '''

    def has_content(self):
        return 0

    def to_html(self, ctx):
        pass


# Content objects are the basic building block of the parsed HTML
# template.  Converting a Content object to HTML simply converts each
# item in the object to HTML.  All content enclosing tags use Content
# objects to store their content and then use the special tag logic to
# control when and how often the content should be sent as output.

class Content:

    '''Manage a list of content items.
    '''

    def __init__(self):
        self.items = []

    def __repr__(self):
        return 'Content(%s)' % self.items

    def append(self, item):
        self.items.append(item)

    def to_html(self, ctx):
        for item in self.items:
            item.to_html(ctx)

# Tags such as <al-in>, <al-form>, <al-tree>, <al-expand> do
# enclose content.
# 
# When you subclass the EnclosingTag class you must supply a name
# class attribute and you should almost certainly override the
# to_html() method.  For example, the following is very simple if:
# 
# class If(template.ContentTag):
#     name = 'al-if'
#
#     def __init__(self, ctx, filename, line_num, attribs):
#         template.ContentTag.__init__(self, ctx, filename, line_num, attribs)
#         if not self.has_attrib('expr'):
#             self.raise_error('missing "expr" attribute')
#         self.expr = compile(self.get_attrib('expr'), '<albatross>', 'eval')
#
#     def to_html(self, ctx):
#         if ctx.eval_expr(self.expr):
#             template.ContentTag.to_html(self, ctx)

class EnclosingTag(Tag):

    '''All tags which enclose content inherit from this class
    '''

    def __init__(self, ctx, filename, line_num, attribs):
        Tag.__init__(self, ctx, filename, line_num, attribs)
        self.content = Content()

    def __repr__(self):
        return Tag.__repr__(self) + repr(self.content) + '</%s>' % self.name

    def has_content(self):
        return 1

    def append(self, item):
        self.content.append(item)

    def to_html(self, ctx):
        self.content.to_html(ctx)


# A Template object contains the parsed HTML content of a single
# template file.  Loading a template parses the HTML and compiles the
# expressions within the file.  The execution phase happens later once
# the application has established an execution context.
# 
# A tag "manager" is passed in which resolves the tag name to a Python
# class which implements that tag.  This allows the application to
# extend the standard tag set without changing the toolkit.

# Parsing works by splitting the file into a stream of text, tag,
# text, tag, ...  The current parent object for content is maintained
# on a stack, each time a new content enclosing tag is encountered,
# the current parent is saved and all new content is directed to the
# new parent.  When the close tag is encountered the old parent is
# popped off the stack.

# Locate tags; remember start, attribs, end
_sre_dblquote_nogrp = r'"(?:[^"\\]*(?:\\.[^"\\]*)*)"'   # eg, "hello \"bill\""
_sre_sglquote_nogrp = r"'(?:[^'\\]*(?:\\.[^'\\]*)*)'"   # eg, 'sam\'s ball'
_sre_attr = r'(?:' + \
    r'\s+\w+=' + _sre_dblquote_nogrp + '|' + \
    r'\s+\w+=' + _sre_sglquote_nogrp + '|' + \
    r'\s+\w+' + ')'                                     # eg, attr="value"
_sre_attrs = r'(' + _sre_attr + r'*)'                   # eg, a1="v1" a2="v2"
_sre_tagname = r'(/?alx?-\w+)'                          # eg, al-value
_sre_tag = r'<'+_sre_tagname+_sre_attrs+r'(\s*/?>)'     # a full albatross tag
_re_tag = re.compile(_sre_tag, re.I|re.M|re.S)

_re_leading = re.compile(r'^\s+')

# Extract attributes
_sre_dblquote_grp = r'"([^"\\]*(?:\\.[^"\\]*)*)"'
_sre_sglquote_grp = r"'([^'\\]*(?:\\.[^'\\]*)*)'"
_sre_attr_grp = r'(\w+)=(?:'+_sre_dblquote_grp+'|'+_sre_sglquote_grp+')|(\w+)'
_re_attrib = re.compile(_sre_attr_grp, re.I|re.M)

_re_dequote = re.compile(r'\\(.)')


class Template:

    '''Manage a single HTML template file
    '''

    def __init__(self, ctx, filename, text):
        '''The ctx argument is is used to resolve tag names to tag
        classes.  filename is to identify where the template text came
        from.

        The the file identified by filename is never referenced to
        allow external code to implement caching schemes.  It also
        allows you to do things like this:

        t = Template(tags, "<cache>",
        """<al-for iter="i" expr="range(20)">
          <al-value expr="i.value">
        </al-for>""")
        '''
        TAG_TEXT = 0
        TAG_NAME = 1
        TAG_ATTRIBS = 2
        TAG_CLOSE = 3
        WHITE_ALL = 'all'
        WHITE_STRIP = 'strip'
        WHITE_INDENT = 'indent'
        WHITE_NEWLINE = 'newline'

        state = TAG_TEXT
        white_space = WHITE_STRIP
        self.content = Content()
        parent_stack = []
        parent = self.content
        line_num = 1
        for part in _re_tag.split(text):
            if state == TAG_TEXT:
                str = part
                if str:
                    if white_space == WHITE_STRIP:
                        if str[0] == '\n':
                            str = _re_leading.sub('', str)
                    elif white_space == WHITE_INDENT:
                        if str[0] == '\n':
                            str = str[1:]
                    elif white_space == WHITE_NEWLINE:
                        str = '\n'
                if str:
                    parent.append(Text(str))
                white_space = WHITE_STRIP
                state = TAG_NAME
            elif state == TAG_NAME:
                name = part.lower()
                if name[0] == '/':
                    # close tag - pop parent
                    name = name[1:]
                    if not parent_stack:
                        raise ApplicationError('%s:%s: "%s"; unexpected "%s" end tag' % (filename, line_num, part, name))
                    if parent.name != name:
                        raise ApplicationError('%s:%s: "%s"; expected "%s" end tag' % (filename, line_num, part, parent.name))
                    parent = parent_stack[-1]
                    del parent_stack[-1]
                    is_open = 0
                else:
                    # open tag - find class
                    tagclass = ctx.get_tagclass(name)
                    if not tagclass:
                        raise ApplicationError('%s:%s: undefined tag "%s"' % \
                                               (filename, line_num, name))
                    is_open = 1
                state = TAG_ATTRIBS
            elif state == TAG_ATTRIBS:
                bits = _re_attrib.split(part)
                dict = {}
                attrib_order = []
                offset = 0
                while 1:
                    attrbits = bits[offset: offset + 5]
                    if len(attrbits) < 5:
                        break
                    offset = offset + 5
                    white, name1, value1, value2, name2 = attrbits
                    if name1:
                        name = name1.lower()
                        if value1 is not None:
                            value = _re_dequote.sub(r'\1', value1)
                        else:
                            value = _re_dequote.sub(r'\1', value2)
                    else:
                        name = name2.lower()
                        value = None
                    if name == 'whitespace':
                        if value not in (WHITE_ALL, WHITE_STRIP, WHITE_INDENT,
                                         WHITE_NEWLINE, None):
                            raise ApplicationError('%s:%s: illegal whitespace value "%s"' % \
                                                   (filename, line_num, value))
                        white_space = value
                    else:
                        dict[name] = value
                        attrib_order.append(name)
                if is_open:
                    tag = tagclass(ctx, filename, line_num, dict)
                    tag.set_attrib_order(attrib_order)
                    parent.append(tag)
                    if tag.has_content():
                        parent_stack.append(parent)
                        parent = tag
                state = TAG_CLOSE
            elif state == TAG_CLOSE:
                if part.endswith('/>'):
                    if tag.has_content():
                        parent = parent_stack[-1]
                        del parent_stack[-1]
                else:
                    pass
                state = TAG_TEXT
            line_num = line_num + part.count('\n')
        if state not in (TAG_TEXT, TAG_NAME):
            raise ApplicationError('%s:%s: incomplete tag definition' % \
                                   (filename, line_num))
        if parent_stack:
            raise ApplicationError('%s:%s: missing close tag for "%s"' % \
                                   (filename, parent.line_num, parent.name))

    def __repr__(self):
        return 'Template(%s)' % self.content

    def to_html(self, ctx):
        '''Execute template within an execution context.
        '''
        self.content.to_html(ctx)


if __name__ == '__main__':
    import sys
    from albatross import context
    ctx = context.SimpleContext('../test')
    t = Template(ctx, '<ether>', '''
<al-lookup name="xlat">
 <al-item expr="0">Zero</al-item>
 <al-item expr="1">One</al-item>
 <al-item expr="2">Two</al-item>
 Many</al-lookup>
<al-for iter="i" expr="range(12)">
 <al-value expr="i.value()" lookup="xlat" whitespace>
</al-for>
''')
    t.to_html(ctx)
    ctx.flush_content()
