
#
# Template (Version 2.5)
#
# Klasse zum Parsen von Templates
#
# Autor:            Patrick Canterino <patrick@patshaping.de>
# Letzte Aenderung: 26.4.2013
#
# Copyright (C) 2002-2013 Patrick Canterino
#
# Diese Datei kann unter den Bedingungen der "Artistic License 2.0"
# weitergegeben und / oder veraendert werden.
# Siehe:
# http://www.opensource.org/licenses/artistic-license-2.0
#

import os.path
import sys

class Template:

    # __init__()
    #
    # Konstruktor
    #
    # Parameter: -keine-
    #
    # Rueckgabe: -nichts-

    def __init__(self):
        self.file         = ''
        self.template     = ''
        self.original     = ''
        self.old_parsing  = 0
        self.vars         = {}
        self.defined_vars = []
        self.loop_vars    = {}

    # get_template()
    #
    # Kompletten Vorlagentext zurueckgeben
    #
    # Parameter: -keine-
    #
    # Rueckgabe: Kompletter Vorlagentext (String)

    def get_template(self):
        return str(self.template)

    # set_template()
    #
    # Kompletten Vorlagentext aendern
    #
    # Parameter: Vorlagentext
    #
    # Rueckgabe: -nichts- (Template-Objekt wird modifiziert)

    def set_template(self,template):
        self.template = str(template)

    # add_text()
    #
    # Vorlagentext ans Template-Objekt anhaengen
    #
    # Parameter: Vorlagentext
    #
    # Rueckgabe: -nichts- (Template-Objekt wird modifiziert)

    def add_text(self,text):
        self.set_template(self.get_template()+str(text))

    # read_file()
    #
    # Einlesen einer Vorlagendatei und {INCLUDE}-Anweisungen ggf. verarbeiten
    # (Text wird an bereits vorhandenen Text angehaengt)
    #
    # Parameter: 1. Datei zum Einlesen
    #            2. Status-Code (Boolean):
    #               true  => {INCLUDE}-Anweisungen nicht verarbeiten
    #               false => {INCLUDE}-Anweisungen verarbeiten (Standard)
    #
    # Rueckgabe: -nichts- (Template-Objekt wird modifiziert)

    def read_file(self,file,not_include=0):
        self.file = file

        fp = open(file,'r')
        content = fp.read()
        fp.close()

        self.add_text(content)
        self.save_state()

        if not not_include: self.parse_includes()

    # set_var()
    #
    # Wert einer Variable setzen
    #
    # Parameter: 1. Name der Variable
    #            2. Wert, den die Variable erhalten soll
    #
    # Rueckgabe: -nichts- (Template-Objekt wird modifiziert)

    def set_var(self,var,content):
        if content is None: content = ''
        self.vars[var] = content

    # get_var()
    #
    # Wert einer Variable zurueckgeben
    #
    # Parameter: (optional) Variablenname
    #
    # Rueckgabe: Wert der Variable;
    #            wenn die Variable nicht existiert, false;
    #            wenn kein Variablenname angegeben wurde, wird ein
    #            Array mit den Variablennamen zurueckgegeben

    def get_var(self,var=None):
        if var is not None:
            if var in self.vars:
                return self.vars[var]
            else:
                return None
        else:
            return self.vars.keys()

    # set_vars()
    #
    # Komplettes Variablen-Array mit einem anderen Array ueberschreiben
    #
    # Parameter: Array
    #
    # Rueckgabe: -nichts- (Template-Objekt wird modifiziert)

    def set_vars(self,vars):
        self.vars = vars

    # add_vars()
    #
    # Zum bestehenden Variablen-Array weitere Variablen in Form eines Arrays
    # hinzufuegen
    #
    # Parameter: Array
    #
    # Rueckgabe: -nichts- (Template-Objekt wird modifiziert)

    def add_vars(self,vars):
        self.vars.update(vars)

    # set_loop_data()
    #
    # Daten fuer eine Schleife setzen
    #
    # Parameter: 1. Name der Schleife
    #            2. Array mit den Dictionaries mit den Variablen fuer
    #               die Schleifendurchgaenge
    #
    # Rueckgabe: -nichts- (Template-Objekt wird modifiziert)

    def set_loop_data(self,loop,data):
        self.loop_vars[loop] = data

    # add_loop_data()
    #
    # Daten fuer einen Schleifendurchgang hinzufuegen
    #
    # Parameter: 1. Name der Schleife
    #            2. Dictionary mit den Variablen fuer den
    #               Schleifendurchgang
    #
    # Rueckgabe: -nichts- (Template-Objekt wird modifiziert)

    def add_loop_data(self,loop,data):
        if loop in self.loop_vars and type(self.loop_vars[loop]) is list:
            self.loop_vars[loop].append(data)
        else:
            self.loop_vars[loop] = [data]

    # parse()
    #
    # In der Template definierte Variablen auslesen, Variablen
    # ersetzen, {IF}- und {TRIM}-Bloecke parsen
    #
    # Parameter: -nichts-
    #
    # Rueckgabe: -nichts- (Template-Objekt wird modifiziert)

    def parse(self):
        if self.old_parsing: return self.parse_old()

        # Zuerst die Schleifen parsen

        if self.loop_vars and self.loop_vars.keys():
            loops = self.loop_vars.keys()

            for loop in loops:
                self.parse_loop(loop)

        # In Template-Datei definierte Variablen auslesen

        self.get_defined_vars()

        # Variablen ersetzen

        vars = self.get_var()

        if vars is not None and len(vars) > 0:
            self.parse_if_blocks()
            self.replace_vars()

        # {TRIM}-Bloecke entfernen

        self.parse_trim_blocks()

    # parse_old()
    #
    # In der Template definierte Variablen auslesen, Variablen
    # ersetzen, {IF}- und {TRIM}-Bloecke parsen
    # (alte Methode)
    #
    # Parameter: -nichts-
    #
    # Rueckgabe: -nichts- (Template-Objekt wird modifiziert)

    def parse_old(self):
        # Zuerst die Schleifen parsen

        if self.loop_vars and self.loop_vars.keys():
            loops = self.loop_vars.keys()

            for loop in loops:
                self.parse_loop(loop)

        # Normale Variablen durchgehen

        if self.get_var():
            vars = self.get_var()

            for var in vars:
                val = self.get_var(var)

                self.parse_if_block(var,val)

                if type(val) is list:
                    self.fillin_array(var,val)
                else:
                    self.fillin(var,val)

        # Jetzt dasselbe mit denen, die direkt in der Template-Datei    definiert
        # sind, machen. Ich weiss, dass das eine ziemlich unsaubere Loesung ist,
        # aber es funktioniert

        self.get_defined_vars()

        for var in self.defined_vars:
            val = self.get_var(var)

            self.parse_if_block(var,val)
            self.fillin(var,val)

        # {TRIM}-Bloecke entfernen

        self.parse_trim_blocks()

    # fillin()
    #
    # Variablen durch Text ersetzen
    #
    # Parameter: 1. Variable zum Ersetzen
    #            2. Text, durch den die Variable ersetzt werden soll
    #
    # Rueckgabe: -nichts- (Template-Objekt wird modifiziert)

    def fillin(self,var,text):
        if text is None: text = ''

        template = self.get_template();
        template = template.replace('{'+str(var)+'}',str(text));

        self.set_template(template);

    # fillin_array()
    #
    # Variable durch Array ersetzen
    #
    # Parameter: 1. Variable zum Ersetzen
    #            2. Array, durch das die Variable ersetzt werden soll
    #            3. Zeichenkette, mit der das Array verbunden werden soll
    #               (Standard: '')
    #
    # Rueckgabe: -nichts- (Template-Objekt wird modifiziert)

    def fillin_array(self,var,array,glue=''):
        self.fillin(var,str(glue).join(array))

    # replace_vars()
    #
    # Variablen eine nach der anderen ersetzen. Sollte in einer Variable eine
    # andere Variable auftauchen, so wird diese nicht ersetzt.
    #
    # Parameter: Array mit zu parsenden Variablen (optional)
    #
    # Rueckgabe: -nichts- (Template-Objekt wird modifiziert)

    def replace_vars(self,valid=None):
        template = self.get_template()

        if valid is None:
            valid_vars = self.get_var()
        else:
            valid_vars = valid

        x = 0

        while x < len(template):
            if template[x] == '{':
                for var in valid_vars:
                    # Pruefen, ob hier eine gueltige Variable beginnt

                    if template[x+1:x+len(var)+2] == var + '}':
                        if type(self.get_var(var)) is list:
                            content = ''.join(self.get_var(var))
                        else:
                            # Muss es nochmal zum String machen
                            # Hilft gegen den "ordinal not in range(128)"-Fehler
                            # Habe aber keine Ahnung, welche neuen Probleme das verursachen koennte
                            # Bin gespannt...
                            content = str(self.get_var(var))

                        if content is None: content = ''

                        # Daten vor und nach der Variable

                        pre  = template[0:x]
                        post = template[len(pre)+2+len(var):]

                        # Alles neu zusammensetzen

                        template = pre + content + post

                        # Zaehler aendern

                        x = len(pre + content) - 1

            x += 1

        self.set_template(template)

    # to_file()
    #
    # Template in Datei schreiben
    #
    # Parameter: Datei-Handle
    #
    # Rueckgabe: Status-Code (Boolean)

    def to_file(self,handle):
        return handle.write(self.get_template())

    # reset()
    #
    # Den gesicherten Stand des Template-Textes wiederherstellen
    #
    # Parameter: -nichts-
    #
    # Rueckgabe: -nichts- (Template-Objekt wird modifiziert)

    def reset(self):
        self.template = self.original

    # save_state()
    #
    # Aktuellen Stand des Template-Textes sichern
    # (alte Sicherung wird ueberschrieben)
    #
    # Parameter: -nichts-
    #
    # Rueckgabe: -nichts- (Template-Objekt wird modifiziert)

    def save_state(self):
        self.original = self.template

    # parse_loop()
    #
    # Eine Schleife parsen
    #
    # Parameter: Name der Schleife
    #
    # Rueckgabe: -nichts- (Template-Objekt wird modifiziert)

    def parse_loop(self,name):
        template = self.get_template()
        if template.find('{LOOP '+name+'}') == -1: return

        offset     = 0
        name_len = len(name)

        while template.find('{LOOP '+name+'}',offset) != -1:
            begin = template.find('{LOOP '+name+'}',offset)

            if template.find('{ENDLOOP}',begin+6+name_len) != -1:
                end = template.find('{ENDLOOP}',begin+6+name_len)

                block    = template[begin:end+9]
                content = block[name_len+7:-9]

                parsed_block = ''

                x = 0

                while x < len(self.loop_vars[name]):
                    loop_data = self.loop_vars[name][x]
                    loop_vars = loop_data.keys()

                    ctpl = Template()
                    ctpl.set_template(content)

                    for loop_var in loop_vars:
                        ctpl.set_var(name+'.'+loop_var,loop_data[loop_var])

                    if self.old_parsing:
                        ctpl.parse_old()
                    else:
                        ctpl.parse()

                    parsed_block += ctpl.get_template()

                    del(ctpl)
                    x += 1

                template = template.replace(block,parsed_block)
                offset   = begin+len(parsed_block)

            else:
                break

        self.set_template(template)

    # get_defined_vars()
    #
    # In der Template-Datei definierte Variablen auslesen
    #
    # Parameter: -nichts-
    #
    # Rueckgabe: -nichts- (Template-Objekt wird modifiziert)

    def get_defined_vars(self):
        template = self.get_template()
        if template.find('{DEFINE ') == -1: return

        offset = 0

        while template.find('{DEFINE ',offset) != -1:
            begin  = template.find('{DEFINE ',offset)+8
            offset = begin

            name    = ''
            content = ''

            var_open     = 0
            name_found   = 0
            define_block = 0

            x = begin

            while x < len(template):
                if template[x] == '\012' or template[x] == '\015':
                    # Wenn in einem {DEFINE}-Block ein Zeilenumbruch gefunden wird,
                    # brechen wir mit dem Parsen des Blockes ab

                    break

                if var_open == 1:
                    if template[x] == '"':
                        # Der Inhalt der Variable ist hier zu Ende

                        var_open = 0

                        if template[x+1] == '}':
                            # Hier ist der Block zu Ende

                            if self.get_var(name) is None:
                                # Die Variable wird nur gesetzt, wenn sie nicht bereits gesetzt ist

                                self.set_var(name,content)
                                self.defined_vars.append(name)

                            # {DEFINE}-Block entfernen

                            pre  = template[0:begin-8]
                            post = template[x+2:]

                            template = pre+post

                            # Fertig!

                            offset = len(pre)
                            break

                    elif template[x] == '\\':
                        # Ein Backslash wurde gefunden, er dient zum Escapen von Zeichen

                        if template[x+1] == 'n':
                            # "\n" in Zeilenumbrueche umwandeln

                            content += "\n"
                        else:
                            content += template[x+1]

                        x += 1

                    else:
                        content += template[x]

                else:
                    if name_found == 1:
                        if var_open == 0:
                            if template[x] == '"':
                                var_open = 1
                            else:
                                break

                    else:
                        # Variablennamen auslesen

                        if template[x] == '}' and name != '':
                            # Wir haben einen {DEFINE}-Block

                            name_found      = 1
                            define_found = 1

                            # Alles ab hier sollte mit dem Teil verbunden werden, der das
                            # {DEFINE} in einer Zeile verarbeitet

                            # Der Parser fuer {DEFINE}-Bloecke ist nicht rekursiv, was auch
                            # nicht noetig sein sollte

                            if template.find('{ENDDEFINE}',x) != -1:
                                end = template.find('{ENDDEFINE}',x)
                                x += 1

                                content = template[x:end]

                                if self.get_var(name) is None:
                                    # Die Variable wird nur gesetzt, wenn sie nicht bereits gesetzt ist

                                    self.set_var(name,content)
                                    self.defined_vars.append(name)

                                pre  = template[0:begin-8]
                                post = template[end+11:]

                                template = pre + post

                                # Fertig!

                                offset = len(pre)
                                break

                            else:
                                break

                        elif template[x] != ' ':
                            name += template[x]

                        elif template[x] != '':
                            name_found = 1

                        else:
                            break

                x += 1

        self.set_template(template)

    # parse_if_block()
    #
    # IF-Bloecke verarbeiten
    #
    # Parameter: 1. Name des IF-Blocks (das, was nach dem IF steht)
    #            2. Status-Code (true  => Inhalt anzeigen
    #                            false => Inhalt nicht anzeigen
    #
    # Rueckgabe: -nichts- (Template-Objekt wird modifiziert)

    def parse_if_block(self,name,state,no_negate=0):
        name     = str(name)
        template = self.get_template()

        count = 0;

        while template.find('{IF '+name+'}') >= 0:
            count += 1

            start    = template.find('{IF '+name+'}')
            tpl_tmp  = template[start:]
            splitted = tpl_tmp.split('{ENDIF}')

            block = '' # Kompletter bedingter Block
            ifs   = 0  # IF-Zaehler (wird fuer jedes IF erhoeht und fuer jedes ENDIF erniedrigt)

            # {IF}

            x = 0

            while x < len(splitted):
                # Verschachtelungsfehler abfangen
                if x == len(splitted)-1: raise TplClassIFNestingError(self.file,name,count)

                ifs += splitted[x].count('{IF ') # Zum Zaehler jedes Vorkommen von IF hinzuzaehlen
                ifs -= 1                         # Zaehler um 1 erniedrigen
                block += splitted[x]+'{ENDIF}'   # Daten zum Block hinzufuegen

                x += 1

                if ifs == 0:
                    # Zaehler wieder 0, also haben wir das Ende des IF-Blocks gefunden :-))
                    break

            if_block = block[len(name)+5:-7] # Alles zwischen {IF} und {ENDIF}

            # {ELSE}

            else_block = '' # Alles ab {ELSE}
            ifs        = 0    # IF-Zaehler

            splitted = if_block.split('{ELSE}');

            x = 0

            while x < len(splitted):
                ifs += splitted[x].count('{IF ')    # Zum Zaehler jedes Vorkommen von IF hinzuzaehlen
                ifs -= splitted[x].count('{ENDIF}') # Vom Zaehler jedes Vorkommen von ENDIF abziehen

                x += 1

                if ifs == 0:
                    # Zaehler 0, also haben wir das Ende des IF-Abschnitts gefunden

                    # Aus dem Rest den ELSE-Block zusammenbauen

                    y = x

                    while y < len(splitted):
                        else_block += '{ELSE}'+splitted[y]
                        y += 1

                    if else_block:
                        if_block   = if_block[0:len(if_block)-len(else_block)]
                        else_block = else_block[6:]

                    break

            if state:
                replacement = if_block
            else:
                replacement = else_block

            template = template.replace(block,replacement)

        self.set_template(template)

        # Evtl. verneinte Form parsen

        if not no_negate:
            self.parse_if_block('!'+name,not state,1)

    # parse_if_blocks()
    #
    # IF-Bloecke zu allen definierten Variablen verarbeiten
    #
    # Parameter: Array mit zu verarbeitenden IF-Bloecken (optional)
    #
    # Rueckgabe: -nichts- (Template-Objekt wird modifiziert)

    def parse_if_blocks(self,valid=None):
        if valid is None:
            valid_vars = self.get_var()
        else:
            valid_vars = valid

        for valid_var in valid_vars:
            self.parse_if_block(valid_var,self.get_var(valid_var))

    # parse_trim_blocks()
    #
    # {TRIM}-Bloecke parsen
    #
    # Dieser Parser ist nicht rekursiv, was auch nicht
    # noetig sein sollte.
    #
    # Parameter: -nichts-
    #
    # Rueckgabe: -nichts- (Template-Objekt wird modifiziert)

    def parse_trim_blocks(self):
        template = self.get_template()
        if template.find('{TRIM}') == -1: return

        offset = 0

        while template.find('{TRIM}',offset) >= 0:
            begin = template.find('{TRIM}',offset)

            if template.find('{ENDTRIM}',begin+6) >= 0:
                end = template.find('{ENDTRIM}',begin+6)

                block    = template[begin:end+9]
                content  = block[6:-9]

                trimmed  = content.strip()

                template = template.replace(block,trimmed)

                offset   = begin+len(trimmed)
            else:
                break

        self.set_template(template)

    # parse_condtag()
    #
    # Bedingungstags in einem Vorlagentext verarbeiten
    #
    # Parameter: 1. Tagname
    #            2. Status-Code (true  => Tag-Inhalt anzeigen
    #                            false => Tag-Inhalt nicht anzeigen
    #
    # Rueckgabe: -nichts- (Template-Objekt wird modifiziert)

    def parse_condtag(self,condtag,state):
        condtag  = str(condtag)
        template = self.get_template()

        while template.find('<'+condtag+'>') >= 0:
            start   = template.find('<'+condtag+'>')                 # Beginn des Blocks
            end     = template.find('</'+condtag+'>')+len(condtag)+3 # Ende des Blocks

            extract = template[start:end]                            # Kompletten Bedingungsblock extrahieren...

            if state:
                replacement = extract[len(condtag)+2:0-len(condtag)-3]
            else:
                replacement = ''

            template = template.replace(extract,replacement)         # Block durch neue Daten ersetzen

        self.set_template(template)

    # parse_includes()
    #
    # {INCLUDE}-Anweisungen verarbeiten
    #
    # Parameter: -nichts-
    #
    # Rueckgabe: -nichts- (Template-Objekt wird modifiziert)

    def parse_includes(self):
        template = self.get_template()
        if template.find('{INCLUDE ') == -1: return

        offset = 0

        while template.find('{INCLUDE ',offset) >= 0:
            begin = template.find('{INCLUDE ',offset)

            start  = begin+9
            offset = start
            long   = 0

            if template[start] == '"':
                long = 1
                start += 1

            file = ''
            skip = 0

            x = start

            while x < len(template):
                if template[x] == '\012' or template[x] == '\015':
                    skip = 1
                    break
                elif long == 0 and template[x] == ' ':
                    skip = 1
                    break
                elif long == 1 and template[x] == '"':
                    if template[x+1] != '}': skip = 1
                    break
                elif long == 0 and template[x] == '}':
                    break
                else:
                    file += template[x]

                x += 1

            if skip == 1: continue

            if file != '':
                filepath = file

                if not os.path.isabs(file):
                    dir  = os.path.dirname(self.file)
                    if not dir: dir = '.'
                    filepath = os.path.normpath(dir+'/'+file)

                if os.path.isfile(filepath):
                    inc = Template()
                    inc.read_file(file)

                    if long == 1: end = start + len(file) + 2
                    else:         end = start + len(file) + 1

                    pre  = template[0:begin]
                    post = template[end:]

                    template = pre+inc.get_template()+post
                    offset   = len(pre)+len(inc.get_template())

                    del(inc)

        self.set_template(template)

# Klasse zum Erzeugen des Fehlers bei falsch verschachtelten
# {IF}-Bloecken

class TplClassIFNestingError:

    def __init__(self,file,name,count):
        self.file  = file
        self.name  = name
        self.count = count

    def __str__(self):
        return 'Nesting error found while parsing IF block "'+self.name+'" nr. '+str(self.count)+' in template file "'+self.file+'"'

#
### Ende ###