1"""
2Permission is hereby granted, free of charge, to any person obtaining a copy
3of this software and associated documentation files (the "Software"), to deal
4in the Software without restriction, including without limitation the rights
5to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
6copies of the Software, and to permit persons to whom the Software is
7furnished to do so, subject to the following conditions:
8
9The above copyright notice and this permission notice shall be included in all
10copies or substantial portions of the Software.
11
12THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
13IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
14FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
15AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
16LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
18SOFTWARE.
19
20This file is originally from: https://bitbucket.org/hpk42/py, specifically:
21https://bitbucket.org/hpk42/py/src/980c8d526463958ee7cae678a7e4e9b054f36b94/py/_xmlgen.py?at=default
22by holger krekel, holger at merlinux eu. 2009
23"""
24import sys
25import re
26
27if sys.version_info >= (3, 0):
28    def u(s):
29        return s
30
31    def unicode(x):
32        if hasattr(x, '__unicode__'):
33            return x.__unicode__()
34        return str(x)
35else:
36    def u(s):
37        return unicode(s)
38    unicode = unicode
39
40
41class NamespaceMetaclass(type):
42
43    def __getattr__(self, name):
44        if name[:1] == '_':
45            raise AttributeError(name)
46        if self == Namespace:
47            raise ValueError("Namespace class is abstract")
48        tagspec = self.__tagspec__
49        if tagspec is not None and name not in tagspec:
50            raise AttributeError(name)
51        classattr = {}
52        if self.__stickyname__:
53            classattr['xmlname'] = name
54        cls = type(name, (self.__tagclass__,), classattr)
55        setattr(self, name, cls)
56        return cls
57
58
59class Tag(list):
60
61    class Attr(object):
62
63        def __init__(self, **kwargs):
64            self.__dict__.update(kwargs)
65
66    def __init__(self, *args, **kwargs):
67        super(Tag, self).__init__(args)
68        self.attr = self.Attr(**kwargs)
69
70    def __unicode__(self):
71        return self.unicode(indent=0)
72    __str__ = __unicode__
73
74    def unicode(self, indent=2):
75        l = []
76        SimpleUnicodeVisitor(l.append, indent).visit(self)
77        return u("").join(l)
78
79    def __repr__(self):
80        name = self.__class__.__name__
81        return "<%r tag object %d>" % (name, id(self))
82
83Namespace = NamespaceMetaclass('Namespace', (object, ), {
84    '__tagspec__': None,
85    '__tagclass__': Tag,
86    '__stickyname__': False,
87})
88
89
90class HtmlTag(Tag):
91
92    def unicode(self, indent=2):
93        l = []
94        HtmlVisitor(l.append, indent, shortempty=False).visit(self)
95        return u("").join(l)
96
97# exported plain html namespace
98
99
100class html(Namespace):
101    __tagclass__ = HtmlTag
102    __stickyname__ = True
103    __tagspec__ = dict([(x, 1) for x in (
104        'a,abbr,acronym,address,applet,area,b,bdo,big,blink,'
105        'blockquote,body,br,button,caption,center,cite,code,col,'
106        'colgroup,comment,dd,del,dfn,dir,div,dl,dt,em,embed,'
107        'fieldset,font,form,frameset,h1,h2,h3,h4,h5,h6,head,html,'
108        'i,iframe,img,input,ins,kbd,label,legend,li,link,listing,'
109        'map,marquee,menu,meta,multicol,nobr,noembed,noframes,'
110        'noscript,object,ol,optgroup,option,p,pre,q,s,script,'
111        'select,small,span,strike,strong,style,sub,sup,table,'
112        'tbody,td,textarea,tfoot,th,thead,title,tr,tt,u,ul,xmp,'
113        'base,basefont,frame,hr,isindex,param,samp,var'
114    ).split(',') if x])
115
116    class Style(object):
117
118        def __init__(self, **kw):
119            for x, y in kw.items():
120                x = x.replace('_', '-')
121                setattr(self, x, y)
122
123
124class raw(object):
125    """just a box that can contain a unicode string that will be
126    included directly in the output"""
127
128    def __init__(self, uniobj):
129        self.uniobj = uniobj
130
131
132class SimpleUnicodeVisitor(object):
133    """ recursive visitor to write unicode. """
134
135    def __init__(self, write, indent=0, curindent=0, shortempty=True):
136        self.write = write
137        self.cache = {}
138        self.visited = {}  # for detection of recursion
139        self.indent = indent
140        self.curindent = curindent
141        self.parents = []
142        self.shortempty = shortempty  # short empty tags or not
143
144    def visit(self, node):
145        """ dispatcher on node's class/bases name. """
146        cls = node.__class__
147        try:
148            visitmethod = self.cache[cls]
149        except KeyError:
150            for subclass in cls.__mro__:
151                visitmethod = getattr(self, subclass.__name__, None)
152                if visitmethod is not None:
153                    break
154            else:
155                visitmethod = self.__object
156            self.cache[cls] = visitmethod
157        visitmethod(node)
158
159    # the default fallback handler is marked private
160    # to avoid clashes with the tag name object
161    def __object(self, obj):
162        # self.write(obj)
163        self.write(escape(unicode(obj)))
164
165    def raw(self, obj):
166        self.write(obj.uniobj)
167
168    def list(self, obj):
169        assert id(obj) not in self.visited
170        self.visited[id(obj)] = 1
171        for elem in obj:
172            self.visit(elem)
173
174    def Tag(self, tag):
175        assert id(tag) not in self.visited
176        try:
177            tag.parent = self.parents[-1]
178        except IndexError:
179            tag.parent = None
180        self.visited[id(tag)] = 1
181        tagname = getattr(tag, 'xmlname', tag.__class__.__name__)
182        if self.curindent and not self._isinline(tagname):
183            self.write("\n" + u(' ') * self.curindent)
184        if tag:
185            self.curindent += self.indent
186            self.write(u('<%s%s>') % (tagname, self.attributes(tag)))
187            self.parents.append(tag)
188            for x in tag:
189                self.visit(x)
190            self.parents.pop()
191            self.write(u('</%s>') % tagname)
192            self.curindent -= self.indent
193        else:
194            nameattr = tagname + self.attributes(tag)
195            if self._issingleton(tagname):
196                self.write(u('<%s/>') % (nameattr,))
197            else:
198                self.write(u('<%s></%s>') % (nameattr, tagname))
199
200    def attributes(self, tag):
201        # serialize attributes
202        attrlist = dir(tag.attr)
203        attrlist.sort()
204        l = []
205        for name in attrlist:
206            res = self.repr_attribute(tag.attr, name)
207            if res is not None:
208                l.append(res)
209        l.extend(self.getstyle(tag))
210        return u("").join(l)
211
212    def repr_attribute(self, attrs, name):
213        if name[:2] != '__':
214            value = getattr(attrs, name)
215            if name.endswith('_'):
216                name = name[:-1]
217            if isinstance(value, raw):
218                insert = value.uniobj
219            else:
220                insert = escape(unicode(value))
221            return ' %s="%s"' % (name, insert)
222
223    def getstyle(self, tag):
224        """ return attribute list suitable for styling. """
225        try:
226            styledict = tag.style.__dict__
227        except AttributeError:
228            return []
229        else:
230            stylelist = [x + ': ' + y for x, y in styledict.items()]
231            return [u(' style="%s"') % u('; ').join(stylelist)]
232
233    def _issingleton(self, tagname):
234        """can (and will) be overridden in subclasses"""
235        return self.shortempty
236
237    def _isinline(self, tagname):
238        """can (and will) be overridden in subclasses"""
239        return False
240
241
242class HtmlVisitor(SimpleUnicodeVisitor):
243
244    single = dict([(x, 1) for x in
245                   ('br,img,area,param,col,hr,meta,link,base,'
246                    'input,frame').split(',')])
247    inline = dict([(x, 1) for x in
248                   ('a abbr acronym b basefont bdo big br cite code dfn em font '
249                    'i img input kbd label q s samp select small span strike '
250                    'strong sub sup textarea tt u var'.split(' '))])
251
252    def repr_attribute(self, attrs, name):
253        if name == 'class_':
254            value = getattr(attrs, name)
255            if value is None:
256                return
257        return super(HtmlVisitor, self).repr_attribute(attrs, name)
258
259    def _issingleton(self, tagname):
260        return tagname in self.single
261
262    def _isinline(self, tagname):
263        return tagname in self.inline
264
265
266class _escape:
267
268    def __init__(self):
269        self.escape = {
270            u('"'): u('&quot;'), u('<'): u('&lt;'), u('>'): u('&gt;'),
271            u('&'): u('&amp;'), u("'"): u('&apos;'),
272        }
273        self.charef_rex = re.compile(u("|").join(self.escape.keys()))
274
275    def _replacer(self, match):
276        return self.escape[match.group(0)]
277
278    def __call__(self, ustring):
279        """ xml-escape the given unicode string. """
280        ustring = unicode(ustring)
281        return self.charef_rex.sub(self._replacer, ustring)
282
283escape = _escape()
284