
"""map xml using into other formats.

A MapController maps one XML format to another by recursive tag
translation.

BASIC USAGE

In basic usage initialize a Map Controller

    M = MapController()

and assign substitution strings or MapNodes for each tag.

Then process text using

    translatedtext = M.process(originaltext)

There will be errors if the xml in originaltext do not
match the assigned substitution strings or MapNodes for M.

TOP LEVEL SUBSTITUTION:

Every MapController must have a top level substitution which
defines a substitution location for "%(__content__)s", for
example as in

    M[''] = ''' <html>
         %(__content__)s
         </BODY>
         </html>
         '''

Here the empty tag '' signifies the top level.  In this particular
example the start of the <BODY> tag might be introduced in the translation
of another tag (like <DOCUMENTTITLE>).

SUBSTITUTION STRINGS:

You can either assign a substitution string to a tag name (for
tags with content) or assign a MapNode for tags which either do
not have content or require other special handling (such as
attribute defaults and transforms).

FOR TAGS WITH CONTENT, you can directly assign a string (which
internally is converted into a MapNode), for example

    M["H1"] = "<br><br><font size=+3><b>%(__content__)s</b></font><br>"

In this case the H1 tag has content (<H1>this is the content</H1>) but
no attributes that effect the translation.  Note that the marker
"%(__content__)s" indicates the location of the recursively translated
content.
    <H1>This is the content</H1>
translates to
    <br><br><font size=+3><b>This is the content</b></font><br>

FOR TAGS WITHOUT CONTENT or which require special handling assign
an explicit MapNode

    M["AUTHOR"] = MapNode(None, "<em>Aaron Watters</em>")

the AUTHOR tag has no content and no attributes that effect translation.
    <AUTHOR/>
translates to
    <em>Aaron Watters</em>

CONTAINED TAGS ARE RECURSIVELY TRANSLATED.  Using the translations
declared above
    <H1><AUTHOR/></H1>
becomes
    <br><br><font size=+3><b><em>Aaron Watters</em></b></font><br>

FOR TAGS WITH ATTRIBUTES include the appropriate attributes in the substition
string, as required.  For example,

    M["RETIRED"] = "<em>%(TITLE)s</em> <b>%(__content__)s</b> <em>Emeritas</em>"

The RETIRED tag has content and one attribute (TITLE) which is
required and effects translation.
    <RETIRED TITLE="Dr.">Carl Fungus</RETIRED>
becomes
    <em>Dr.</em> <b>Carl Fungus</b> <em>Emeritas</em>

For

    M["EXCLAMATION"] = "<FONT COLOR="RED"><B>%(TEXT)</B>, <EM>%(RECIPIENT)s</EM></FONT>"

EXCLAMATION has no content and two attributes (TEXT and RECIPIENT) which are required
and effect formatting.
    <EXCLAMATION TEXT="Holy rusty metal" RECIPIENT="Batman"/>
becomes
    <FONT COLOR="RED"><B>Holy rusty metal</B>, <EM>Batman</EM></FONT>

ATTRIBUTE DEFAULTS can be assigned using a MapNode assigned to a tag.
For example an alternate RETIRED tag might assign "Dr." as the default
value of Title.

    R = MapNode("<em>%(TITLE)s</em> <b>%(__content__)s</b> <em>Emeritas</em>")
    R.default["TITLE"] = "Dr."
    M["RETIRED"] =  R

Then using AUTHOR and RETIRED
    <RETIRED><AUTHOR/></RETIRED>
becomes
    <em>Dr.</em> <b><em>Aaron Watters</em></b> <em>Emeritas</em>
(Note the default "Dr." substitution).  I wish ;(.

ATTRIBUTE TRANSFORMS can be assigned using a MapNode.  For
example to upcase the titles of all retirees:

    R = MapNode("<em>%(TITLE)s</em> <b>%(__content__)s</b> <em>Emeritas</em>")
    from string import upper
    R.addTransform(TITLE, upper)
    M["RETIRED"] =  R

Then
    <RETIRED TITLE="Dr.">Carl Fungus</RETIRED>
becomes
    <em>DR.</em> <b>Carl Fungus</b> <em>Emeritas</em>
The MapNode.transformContent(transform) will perform a transformation
on the recursively generated content of a tag (be careful when using
this one!)

MapNodes can alter the translation definitions for tags encountered
in their content.  For example

    M["Title"] = "<H1>%(__content__)s</H1>"
    ...
    C = MapNode("<hr>Chapter<hr> %(__content__)s")
    C["Title"] = "<H3>%(__content__)s</H3>"
    M["Chapter"] = C

Then a title outside of the Chapter context defaults to H1 but
a title inside a Chapter context defaults to H3.

"TAG NAME" OF None provides a "default" substitution
to apply to unknown tags, as in

    M[None] = "" # erase unknown tags
    M[None] = "#(__content__)s" # echo content for unknown tags

A substitution for a parent can refer to a substitution for a child

    # ref to %(TITLE.__content__)s refers to the title of the play (not act or scene)
    M["PLAY"] = ''' <html><head><title>%(TITLE.__content__)s</title></head><BODY>
                <CENTER><H1><FONT FACE="Arial">%(PLAY.TITLE.__content__)s</FONT></H1></CENTER>
         %(__content__)s
         <BODY>
         </html>
         '''

Any substitution can also refer to a "dotted path" to a content in the tree,
but it will get the last one if there is more than one match:

    M["ACT"]["TITLE"] = "<CENTER>%(PLAY.TITLE.__content__)s <H2>%(__content__)s</H2></CENTER>"

Here the top level element is "PLAY".  All "paths" must start at the top level element.

When "non local" uses of an elements __content__ are in use it is likely that the substitution
for the element itself will "vanish"

    M["TITLE"] = "" # __content__ used in top level (element vanishes)
"""

# SET TO RUN HAMLET IN TEST SUITE
DOHAMLET = 1

import types, string

class MapController:
    """Top level controller"""
    joinSeparator = "\n"

    def __init__(self, topLevelSubstitution = "%s", naive=0, eoCB=None):
        self.naive = naive # use nonvalidating parser if naive set
        self.topLevelSubstitution = topLevelSubstitution
        self.topDict = {}
        self.nodemappers = {None: None} # default
        self.eoCB = eoCB

    def __setitem__(self, item, value):
        if type(value) is types.StringType:
            # very simple case, no defaults, no empty case
            value = MapNode(value)
        self.nodemappers[item] = value

    def __getitem__(self, item):
        return self.nodemappers[item]

    def process(self, text):
        if self.naive:
            from reportlab.lib.rparsexml import parsexmlSimple
            parsexml = parsexmlSimple # non validating
        else:
            from reportlab.lib.rparsexml import parsexml
        elts = parsexml(text,eoCB=self.eoCB)
        return self.processParsed(elts)

    def processParsed(self, elts): # added to allow messing with the parsed input if needed
        D = self.topDict = {}
        D["__prefix__"] = ""
        elts = self.clean_elts(elts)
        textout = self.processContent(elts, top_level=1)
        #print `self.topLevelSubstitution`; print; print
        #print type(textout)
        #print textout.__class__
        T = str(textout)
        result = self.topLevelSubstitution % T
        # clean out the topDict
        for k in D.keys():
            del D[k] # possible self reference
        self.topDict = {}
        return result

    def processContent(self, elts, overrides=None, containerdict=None, top_level=1):
        #print "processContent", containerdict
        if containerdict is None:
            containerdict = self.topDict
        if overrides is None:
            overrides = {} # no overrides yet
        telts = type(elts)
        if telts is types.StringType:
            return self.processString(elts)
        #print telts
        if telts is types.TupleType:
            return self.processTuple(elts, overrides, containerdict)
        else:
            L = []
            for e in elts:
                typee = type(e)
                if typee is types.StringType:
                    e2 = self.processString(e)
                else:
                    if typee is not types.TupleType:
                        raise ValueError, "bad content type %s" % typee
                    e2 = self.processTuple(e, overrides, containerdict)
                L.append(e2)
            return self.joinTranslatedContent(L)

    def processTuple(self, e, overrides, containerdict=None):
        tagname = e[0]
        nodemappers = self.nodemappers
        if overrides:
            nodemappers = nodemappers.copy()
            # do overrides
            nodemappers.update(overrides)
        defaultmapper = nodemappers.get(None, None)
        processor = nodemappers.get(tagname, defaultmapper)
        if processor is None:
            raise NameError, "no processor for %s" % repr(tagname)
        e2 = processor.translate(e, self, overrides, containerdict)
        return e2

    def joinTranslatedContent(self, L):
        return DelayedJoin(L, self.joinSeparator)

    def processString(self, elts):
        return elts # default, for now

    def clean_elts(self, elts):
        """optionally do stuff like dispose of all white content"""
        return elts

class DelayedJoin:
    value = None
    def __init__(self, L, s):
        self.L = L
        self.s = s
    def __str__(self):
        #print "IN str'"
        if self.value is not None: return self.value
        L = map(str, self.L)
        self.value = result = string.join(L, self.s)
        # eliminate possible reference loops
        self.L = None
        #print "FINISHED"
        return result

class ExtendedTransform:
    def __call__(self, t, sdict):
        return t

class LazyKW:
    def __init__(self,D):
        self._D = D
    def __str__(self):
        R = []
        for k,v in self._D.items():
            R.append('%s=%s' % (k,repr(v)))
        return ', '.join(R)

class MapNode(MapController):

    def __init__(self, full_substitution_string, empty_substitution_string=None, defaults=None):
        self.full_substitution_string = full_substitution_string
        self.empty_substitution_string = empty_substitution_string
        self.nodemappers = {}
        if defaults is None:
            defaults = {}
        self.defaults = defaults
        # transforms to apply per attribute to the content (eg make it have 2 digits and put a dollar in front)...
        self.transforms = {}

    def addTransform(self, name, t):
        self.transforms[name] = t

    def transformContent(self, t):
        self.addTransform('__content__',t)

    def translate(self, nodetuple, controller, overrides, containerdict=None):
        # add own overrides if present
        if self.nodemappers:
            overrides = overrides.copy()
            overrides.update(self.nodemappers)
        (tagname, attdict, content, extra) = nodetuple
        #tagname = nodedict[0]
        #content = nodedict.get(1, None)
        if not attdict: attdict = {}
        sdict = attdict.copy() # for modification
        prefix = tagname
        # prefix must be set before processing children
        #print "processing", tagname
        #print containerdict
        if containerdict is not None:
            cprefix = containerdict["__prefix__"]
            if cprefix:
                prefix = "%s.%s" % (cprefix, prefix)
        else:
            cprefix = ''
        #print repr(prefix)
        sdict['__prefix__'] = prefix
        sdict['__parent_prefix__'] = cprefix
        sdict['__depth__'] = prefix.count('.')
        sdict['__parent_depth__'] = sdict['__depth__']-1
        sdict['__tag_depth__'] = (prefix+'.').count(tagname+'.')
        sdict['__mapnode__'] = self
        sdict['__controller__'] = controller
        sdict['__overrides__'] = overrides
        sdict['__containerdict__'] = containerdict
        sdict['__nodetuple__'] = nodetuple
        sdict["__tagname__"] = tagname
        defaults = self.defaults
        for name in defaults.keys():
            if not sdict.has_key(name):
                sdict[name] = defaults[name]
        shadow = ShadowDict(tagname, sdict, containerdict)
        sdict['__shadow__'] = shadow
        #if content is None: stop
        if content is not None:
            pcontent = self.MyProcessContent(content, controller, overrides, shadow)
            sdict["__content__"] = pcontent
            # you can refer to TITLE.__content__
            top = controller.topDict
            #print repr(prefix)
            if containerdict is not None:
                containerdict[tagname+".__content__"] = pcontent
            # you can refer to PLAY.TITLE.__content__
            top[prefix+".__content__"] = pcontent
            sstring = self.full_substitution_string
            if sstring is None:
                raise ValueError, "no content allowed for %s" % repr(tagname)
        else:
            sstring = self.empty_substitution_string
            if sstring is None:
                raise ValueError, "content required for %s" % repr(tagname)
        transforms = self.transforms
        for name in transforms.keys():
            if sdict.has_key(name):
                t = transforms[name]
                oldvalue = str(sdict[name]) # IS STRING CONVERSION ALWAYS RIGHT?
                if isinstance(t,ExtendedTransform):
                    sdict[name] = t(oldvalue, sdict)
                else:
                    sdict[name] = t(oldvalue)
        try:
            result = shadow.substitute(sstring) #sstring % sdict
        except:
            raise ValueError, "for tagname %s bad string %s for dict %s" % (
                repr(tagname), repr(sstring), repr(sdict.keys()))
        return result

    def MyProcessContent(self, content, controller, overrides, containerdict):
        """by default ask the global controller to do it"""
        return controller.processContent(content, overrides, containerdict)

class ShadowDict:
    def __init__(self, name, dict=None, parent=None):
        self.name = name
        self.parent = {}
        self.dict = {}
        if parent is not None:
            self.parent = parent
        if dict is not None:
            self.dict = dict
    def __repr__(self):
        """tag like repr (but don't include parent info)"""
        from string import replace, join
        n = self.name
        c = self.get("__content__", None)
        L = ["<%s" % n]
        d = self.dict
        K = d.keys()
        for name in K:
            if name[:2]!="__":
                v = d[name]
                L.append('\n   %s="%s"' % (name, v))
        if c is None:
            L.append("/>")
        else:
            L.append(">\n   ")
            L.append(string.replace(c, "\n", "\n   "))
            L.append("\n</%s>" % n)
        return join(L, "")
    def __getitem__(self, item):
        try:
            return self.dict[item]
        except KeyError:
            # special case: __repr__
            if item=="__repr__":
                return repr(self)
            return self.parent[item]
    def __setitem__(self, item, value):
        self.dict[item] = value
    def has_key(self, item):
        try:
            test = self[item]
        except KeyError:
            return 0
        else:
            return 1
    def get(self, item, default):
        try:
            return self[item]
        except:
            return default
    def namedsetitem(self, name, item, value):
        if self.name == name:
            self[item] = value
        else:
            try:
                parent.namedsetitem(name, item, value)
            except:
                raise ValueError, "can't match name %s" % repr(name)
    def substitute(self, string):
        return LazySubstitution(string, self)

class LazySubstitution:
    value = None
    def __init__(self, string, dictionary):
        self.string = string
        self.dictionary = dictionary
    def __str__(self):
        #print "in LazySubst.__str__"
        if self.value is not None: return self.value
        self.value = result = self.string % self.dictionary
        # now eliminate possible reference loops
        self.dictionary = None
        #print "done"
        return result

# hex colors for html
LIMEGREEN = "#32CD32"
LEMONCHIFFON = "#FFFACD"
LIGHTCORAL = "#F08080"
SKYBLUE = "#87CEEB"
CORNSILK = "#FFF8DC"

def textsection(title):
    L = []
    a = L.append
    a("""<TABLE CELLSPACING="8" CELLPADDING="3" BORDER="0" WIDTH="98%%">""")
    a("""<tr><TH bgcolor=%s align="right"><font face="arial">%s</font></td></tr> """ % (LIMEGREEN, title))
    a("""<TR><TD><font face="arial">%(__content__)s</font></TD></TR>""")
    a("""</TABLE>""")
    return string.join(L, "\n")

def customer_entry(title):
    return """<tr><td align="right" bgcolor="%s">
              <font face="arial"><em>%s</em></font></td>
        <td bgcolor="%s">
              <font face="arial"><b>%s</b></font></td></tr>""" % (
                  LEMONCHIFFON, title, LIGHTCORAL, "%(__content__)s")

charges_table_format = """
<center><TABLE BORDER="0" width=70%%>""" + """
<tr><td bgcolor=#87CEEB ALIGN="CENTER" colspan=4><font face="Arial">  American Fund </font></td></tr>
            <tr><td  bgcolor=#FFF8DC rowspan=3 colspan=2><font face="Arial"> Amount Invested:
            %(AMOUNT_INVESTED)s
            </font></td>
                <td  bgcolor=#FFFACD colspan=2><font face="Arial"> Initial Charge Ratio:
                %(INITIAL_CHARGE_RATIO)s
                </font></td></tr>
            <tr><td  bgcolor=#FFFACD colspan=2><font face="Arial"> Growth Rate:
            %(GROWTH_RATE)s
            </font></td></tr>
            <tr><td  bgcolor=#FFFACD colspan=2><font face="Arial"> Annual Charge Ratio:
            %(ANNUAL_CHARGE_RATIO)s
            </font></td></tr>
<tr><td  bgcolor=#FFF8DC align="right"><font face="Arial"> For year</font></td>
        <td  bgcolor=#FFFACD align=right><font face="Arial"> Invested to Date</font></td>
        <td  bgcolor=#FFFACD align=right><font face="Arial"> Total charges</font></td>
        <td  bgcolor=#FFFACD align=right><font face="Arial"> Valuation</font></td></tr>
        %(__content__)s
</table></CENTER>
"""
charges_row_format = """
<tr><td  bgcolor=#FFF8DC align="right"><font face="Arial">
                     %(YEAR)s
                     </font></td>
                        <td  bgcolor=#FFFACD align="right"><font face="Arial">
                        %(INVESTED_TO_DATE)s
                        </font></td>
                        <td  bgcolor=#FFFACD align="right"><font face="Arial">
                        %(EFFECT_OF_CHARGES)s
                        </font></td>
                        <td  bgcolor=#FFFACD align="right"><font face="Arial">
                        %(VALUATION)s</font></td></tr>
                        """

def currencystring(t):
    f = float(t)
    return "%10.2f" % f

def test():
    fn = "samples/toykfd.xml"
    ofn = "samples/toykfd.html"
    text = open(fn).read()
    M = MapController()
    M[""] = """ <center><table border width="98%%" bgcolor="""+SKYBLUE+"""><tr><td><b>
        This is an example Key Features Documented generated
         From the XML source which was in turn derived from the CGI input Parameters.
         </b></td></tr></table></center>
         %(__content__)s
         """
    M["MAXI_ISA_KFD"] = """%(__content__)s""" # for inclusion into a larger document...
    M["PARA"] = """<font face="arial"> %(__content__)s </font><br>"""
    M["ITALIC"] = """<i>%(__content__)s </i>"""
    M["BOLD"] = """<b>%(__content__)s </b>"""
    M["PARA"] = """<font face="arial"> %(__content__)s </font><br>"""
    M["AIMS"] = textsection("Fund Aims")
    M["YOUR_INVESTMENT"] = textsection("About Your Investment")
    M["RISK_FACTORS"] = textsection("Risk Factors")
    M["CHARGES_TEXT"] = textsection("Explanation of Charges")
    M["IMPORTANT_INFO"] = textsection("Important Information")
    M["CUSTOMER_INFO"] = """<TABLE CELLSPACING="8" CELLPADDING="3" BORDER="0" WIDTH="98%%">
                        %(__content__)s
                        </table>"""
    M["CUSTOMER_NAME"] = customer_entry("For Customer Name: ")
    M["BROKER_COMPANY_NAME"] = customer_entry("By Company Name: ")
    M["BROKER_CONTACT_NAME"] = customer_entry("Customer Representative: ")
    M["FUND_NAME"] = customer_entry("Investing in Fund: ")
    M["AMOUNT_INVESTED"] = customer_entry("Investment Amount: ")
    M["AMOUNT_INVESTED"].transformContent(currencystring)
    M["CHARGES_TABLE"] = charges_table_format
    # no content allowed for this one.
    RF = M["TABLE_ROW"] = MapNode(None, charges_row_format)
    RF.addTransform("INVESTED_TO_DATE", currencystring)
    RF.addTransform("EFFECT_OF_CHARGES", currencystring)
    RF.addTransform("VALUATION", currencystring)
    otext = M.process(text)
    open(ofn, "w").write(otext)
    print "wrote", ofn

def htest():
    fn = "samples/hamlet.xml"
    ofn = "samples/hamlet.html"
    text = open(fn).read()
    M = MapController()
    # ref to %(TITLE.__content__)s should refer to the title of the play (not act or scene)
    M["PLAY"] = """ <html><head><title>%(TITLE.__content__)s</title></head><BODY>
                <CENTER><H1><FONT FACE="Arial">%(PLAY.TITLE.__content__)s</FONT></H1></CENTER>
         %(__content__)s
         <BODY>
         </html>
         """
    M[""] = "%(__content__)s"
    # TITLE VANISHES IT'S __CONTENT__ IS USED IN THE TOP LEVEL SUBSTITUTION
    M["TITLE"] = ""
##    # top level title
##    M["TITLE"] = """<HEAD>
##                <TITLE>%(__content__)s</TITLE>
##                </HEAD>
##                <BODY>
##                <CENTER><H1><FONT FACE="Arial">%(__content__)s</FONT></H1></CENTER>
##                """
    M["FM"] = """<BR><BR><BR>
                 <CENTER><TABLE><TR><TD BGCOLOR="#99A9FF">
                 %(__content__)s
                 </TD></TR></TABLE></CENTER>
                 """
    M["P"] = """<BR>%(__content__)s<BR>\n"""
    M["PERSONAE"] = """<BR><BR><BR>
                 <CENTER><TABLE>
                 %(__content__)s
                 </TABLE></CENTER>
                 """
    # override title
    M["PERSONAE"]["TITLE"] = "<CENTER><H3>%(PLAY.TITLE.__content__)s %(__content__)s</H3></CENTER>"
    M["PERSONA"] = """<TR><TD BGCOLOR="#A9FF99">%(__content__)s</TD></TR>\n"""
    M["PGROUP"] = """<TR><TD><HR></TR></TD>
                    %(__content__)s
                    <TR><TD><HR></TR></TD>"""
    M["GRPDESCR"] = """<TR><TD ALIGN="RIGHT"><EM>%(__content__)s</EM></TD></TR>"""
    M["SCNDESCR"] = """<CENTER><B>%(__content__)s</B></CENTER>\n"""
    M["PLAYSUBT"] = """<HR><BR><BR>%(__content__)s<BR><BR><HR>\n"""
    M["ACT"] = """%(__content__)s"""
    # override title behaviour
    M["ACT"]["TITLE"] = "<CENTER>%(PLAY.TITLE.__content__)s <H2>%(__content__)s</H2></CENTER>"
    M["SCENE"] = """%(__content__)s"""
    # override title behaviour
    M["SCENE"]["TITLE"] = "<CENTER><H3>%(PLAY.TITLE.__content__)s %(__content__)s</H3></CENTER>"
    THEAD = """<TABLE BORDER="0" WIDTH="98%%"><TR>\n"""
    TTAIL = """\n</TR></TABLE>\n"""
    M["STAGEDIR"] = THEAD + """<TD WIDTH="30%%"></TD><TD ALIGN=RIGHT><em>%(__content__)s</em></TD>\n""" + TTAIL
    M["SPEECH"] = THEAD + "%(__content__)s\n" + TTAIL + "<BR><BR>"
    # A stagedir inside a speech
    M["SPEECH"]["STAGEDIR"] = """<TD></TD><TD ALIGN=RIGHT><em>%(__content__)s</em></TD></TR><TR>\n"""
    M["SPEAKER"] = """<TD width="30%%"><B>%(__content__)s</B>\n</TD><TD></TD></TR><TR>"""
    M["LINE"] = """<TD></TD><TD>%(__content__)s</TD></TR><TR>"""
    M["LINE"]["STAGEDIR"] = "<EM>[%(__content__)s]</EM>"
    otext = M.process(text)
    open(ofn, "w").write(otext)
    print "wrote", ofn

if __name__=="__main__":
    from time import time
    now = time()
    test()
    print "elapsed", time()-now
    if DOHAMLET:
        now = time()
        print 'processing samples/hamlet.xml'
        htest()
        print "elapsed", time()-now
