#!/usr/bin/env python
"""Yet another templating module with support for embedded Python code.

See the end of the module source code and the _test() function for
example usage.

Usage as a script:
    PyEmb.py infile[.pye] [outfile]

    Processes infile and writes output to stdout or outfile if given.

Usage as a CGI script:
    See documentation for the _do_cgi() function.
"""

import os, re, sys
import StringIO

__all__ = ['Error', 'ParseError', 'EmbeddedScript', 'EmbeddedPython',
  'SpecialHTML']
__author__ = "Christopher Arndt <chris.arndt@web.de>"
__version__ = "1.1"

PRINTECHO = 1


class Error(Exception):
    pass
class ParseError(Error):
    pass

def include(fn):
    try:
        f = open(fn)
        s = f.read()
        f.close()
    except:
        return ''
    print s

class EmbeddedScript:
    language = None
    start_rx = re.compile(
      r"""<script\b.*?(language=["']?(?P<lang>\w*)\b.*?)?>""", re.I | re.S)
    end_rx = re.compile(r'</script.*?>', re.I | re.S)
    environment = {}

    def __init__(self, template=None):
        if template:
            self.set_template(template)
        else:
            self.source = ""

    def set_template(self, template):
        self.source = template

    def _parse(self, source):
        i = 0
        script_areas = []
        #print source
        while 1:
            start_m = self.start_rx.search(source, i)
            if start_m:
                if self.language:
                    language = start_m.groupdict().get("lang")
                end_m = self.end_rx.search(source, start_m.end())
                if end_m:
                    if self.language:
                        if not (language and language.lower() \
                          == self.language):
                              break
                    script = source[start_m.end():end_m.start()]
                    span = start_m.start(), end_m.end()
                    script_areas.append((script, span))
                    i = end_m.end()
                else:
                    raise ParseError, "Missing script end tag."
            else:
                break
        return script_areas

    def _subst(self, source=None):
        if source is None:
            source = self.source
        script_areas = self._parse(source)
        env = self._mk_env()
        while script_areas:
            #print script_areas
            i = 0
            output = []
            for t, span in script_areas:
                output.append(source[i:span[0]])
                output.append(self._process(t, env))
                i = span[1]
            output.append(source[i:])
            source = "".join(output)
            script_areas = self._parse(source)
        return source


    def _mk_env(self):
        return self.environment.copy()

    def _process(self, s, env):
        return s

    def __str__(self):
        return self._subst().lstrip()

    def write(self, file=None):
        if type(file) == type(sys.stdout):
            f = file
        elif type(file) == type(""):
            s = str(self)
            if os.path.exists(file):
                if compare_s2f(s, file):
                    f = open(file, 'w')
                    f.write(s)
                    f.close()
                    if PRINTECHO: print "wrote: '%s'" % file
                else:
                    if PRINTECHO: print "file unchanged: '%s" % file
            else:
                f = open(file, 'w')
                f.write(s)
                f.close()
                if PRINTECHO: print "wrote: '%s'" % file
            return
        else:
            f = sys.stdout
        f.write(str(self))


class EmbeddedPython(EmbeddedScript):

    language = "python"

    def _mk_env(self):
        env = self.environment.copy()
        env['file'] = self.filename
        env['include'] = include
        return env

    def set_template(self, template):
        f = open(template)
        self.source = f.read()
        f.close()
        self.filename = template
        self.parsed = 0

    def _process(self, s, env):
        save_stdout = sys.stdout
        sb = StringIO.StringIO()
        sys.stdout = sb
        try:
            sc = self._strip_left(s)
            if sc and sc[0] == "!":
                out = str(eval(sc[1:], env))
            else:
                c = compile(sc, getattr(self, 'filename', '<string>'), 'exec')
                exec c in env
                sb.seek(0)
                out = sb.read().rstrip()
        except:
            import traceback
            out = "<pre>\n"
            out += "".join(apply(traceback.format_exception, sys.exc_info()))
            out += "</pre>\n"
        sys.stdout = save_stdout
        sb.close()
        return out

    def _strip_left(self, s):
        lines = s.splitlines(1)
        edge_width = 0
        for i in xrange(len(lines)):
            if lines[i].strip():
                edge_width = len(lines[i]) - len(lines[i].lstrip())
                break
        l = []
        for line in lines[i:]:
            line = line[edge_width:].rstrip()
            if line:
                l.append(line + '\n')
        return ''.join(l)


# an example of how easy you can change the tags that enclose the script area
class SpecialHTML(EmbeddedPython):
    language = None
    start_rx = re.compile(r"<\[")
    end_rx = re.compile(r"\]>")


# shamelessly ripped from HTMLgen
def compare_s2f(s, f2):
    """Helper to compare a string to a file, return 0 if they are equal."""

    BUFSIZE = 8192
    i = 0
    fp2 = open(f2)
    try:
        while 1:
            try:
                b1 = s[i: i + BUFSIZE]
                i = i + BUFSIZE
            except IndexError:
                b1 = ''
            b2 = fp2.read(BUFSIZE)
            if not b1 and not b2: return 0
            c = cmp(b1, b2)
            if c: return c
    finally:
        fp2.close()


def _do_cgi():
    """CGI support. Put out HTTP header and processed document to stdout.

    The name of the template file is taken from the environment variable
    PATH_TRANSLATED, which should contain the real path of of the template
    file on the server file system. Here's how to set up Apache, so that
    you can directly serve template files (with the extension .pye) and
    process them on-the-fly:

    - Add the following to your 'httpd.conf' or '.htaccess' file:

      AddHandler embedded-python .pye
      Action embedded-python /cgi-bin/PyEmb.py
      AddType text/x-pye .pye

    - Install this module in your cgi-bin directory and make it executable.
    - Restart Apache if necessary.
    """

    try:
        import cgitb; cgitb.enable()
    except: pass

    print "Content-type: text/html\n"
    if os.environ.has_key("PATH_TRANSLATED"):
        d = SpecialHTML(os.environ["PATH_TRANSLATED"])
        d.write()
    else:
        import cgi
        print "<pre>\n" + cgi.escape(_test()) + "</pre>"


def _test():
    """Module test function."""

    d = SpecialHTML()
    d.source = """<[
title = "PyEmb example"
import time
date = time.asctime(time.localtime(time.time()))
]>
<html>
<head>
    <title><[!title]></title>
<head>
A simple equation:
<blockquote>
2 + 2 = <[!2 + 2]>
</blockquote>
<body>
This document was generated by PyEmb on: <[!date]>
<body>
"""
    s = "This is the input document:\n"
    s += "===========================\n"
    s += d.source
    s += "\nThis is the output:\n"
    s += "===================\n"
    s += str(d)
    return s


if __name__ == '__main__':
    if os.environ.has_key('REQUEST_METHOD'):
        _do_cgi()
    elif len(sys.argv) > 1:
        d = SpecialHTML(sys.argv.pop(1))
        if len(sys.argv) > 1:
            d.write(sys.argv.pop(1))
        else:
            d.write()
    else:
        print _test()