0001"""Kid Parser
0002
0003Parses Kid embedded XML to Python source code.
0004"""
0005
0006from __future__ import generators
0007
0008__revision__ = "$Rev: 307 $"
0009__date__ = "$Date: 2006-04-06 11:59:35 +0000 (Thu, 06 Apr 2006) $"
0010__author__ = "Ryan Tomayko (rtomayko@gmail.com)"
0011__copyright__ = "Copyright 2004-2005, Ryan Tomayko"
0012__license__ = "MIT <http://www.opensource.org/licenses/mit-license.php>"
0013
0014import re
0015from kid.pull import *
0016from kid.et import namespaces
0017from kid import Namespace
0018
0019# the kid xml namespace
0020KID_XMLNS = "http://purl.org/kid/ns#"
0021KID_PREFIX = 'py'
0022kidns = Namespace(KID_XMLNS)
0023QNAME_FOR = kidns['for']
0024QNAME_IF = kidns['if']
0025QNAME_DEF = kidns['def']
0026QNAME_SLOT = kidns['slot']
0027QNAME_CONTENT = kidns['content']
0028QNAME_REPLACE = kidns['replace']
0029QNAME_MATCH = kidns['match']
0030QNAME_STRIP = kidns['strip']
0031QNAME_ATTRIBUTES = kidns['attrs']
0032QNAME_EXTENDS = kidns['extends']
0033QNAME_LAYOUT = kidns['layout']
0034
0035# deprectaed
0036QNAME_OMIT = kidns['omit']
0037QNAME_REPEAT = kidns['repeat']
0038
0039# the kid processing instruction name
0040KID_PI = 'python'
0041KID_ALT_PI = 'py'
0042KID_OLD_PI = 'kid'
0043
0044def parse(source, encoding=None, filename=None):
0045    parser = KidParser(document(source, encoding=encoding, filename=filename), encoding)
0046    return parser.parse()
0047
0048def parse_file(filename, encoding=None):
0049    """Parse the file specified.
0050
0051    filename -- the name of a file.
0052    fp       -- an optional file like object to read from. If not specified,
0053                filename is opened.
0054
0055    """
0056    source = open(filename, 'rb')
0057    try:
0058        return parse(source, encoding, filename=filename)
0059    finally:
0060        source.close()
0061
0062class KidParser(object):
0063    def __init__(self, stream, encoding=None):
0064        self.stream = stream
0065        self.encoding = encoding or 'utf-8'
0066        self.depth = 0
0067        self.module_code = CodeGenerator()
0068        self.class_code = CodeGenerator()
0069        self.expand_code = CodeGenerator(level=1)
0070        self.end_module_code = CodeGenerator()
0071        self.module_defs = []
0072        self.inst_defs = []
0073
0074    def parse(self):
0075        self.begin()
0076        self.proc_stream(self.module_code)
0077        self.end()
0078        parts = []
0079        parts += self.module_code.code
0080        for c in self.module_defs:
0081            parts += c.code
0082        parts += self.class_code.code
0083        parts += self.expand_code.code
0084        for c in self.inst_defs:
0085            parts += c.code
0086        parts += self.end_module_code.code
0087        return '\n'.join(parts)
0088
0089    def begin(self):
0090        code = self.module_code
0091        code.line('from __future__ import generators')
0092        code.line('import kid')
0093        code.line('from kid.template_util import *')
0094        code.line('import kid.template_util as template_util')
0095        code.line('_def_names = []')
0096
0097        # default variables. can be overridden by template
0098        code.line('encoding = "%s"' % self.encoding)
0099        code.line('doctype = None')
0100        code.line('omit_namespaces = [kid.KID_XMLNS]')
0101        code.line('layout_params = {}')
0102
0103        # module methods.
0104        code.line('def pull(**kw): return Template(**kw).pull()')
0105        code.line("def generate(encoding=encoding, fragment=0, output=None, **kw): "
0106                  "return Template(**kw).generate(encoding=encoding, fragment=fragment, output=output)")
0107        code.line("def serialize(encoding=encoding, fragment=0, output=None, **kw): "
0108                  "return Template(**kw).serialize(encoding=encoding, fragment=fragment, output=output)")
0109        code.line("def write(file, encoding=encoding, fragment=0, output=None, **kw): "
0110                  "return Template(**kw).write(file, encoding=encoding, fragment=fragment, output=output)")
0111        code.line('BaseTemplate = kid.BaseTemplate')
0112        code.line('def initialize(template): pass')
0113
0114        # expand code
0115        code = self.expand_code
0116        code.start_block('def initialize(self):')
0117        code.line('rslt = initialize(self)')
0118        code.line('if rslt != 0: super(Template, self).initialize()')
0119        code.end_block()
0120        code.start_block('def _pull(self):')
0121        # XXX hack: nasty kluge for making kwargs locals
0122        code.line("exec template_util.get_locals(self, locals())")
0123        code.line('current, ancestors = None, []')
0124        code.line('if doctype: yield (DOCTYPE, doctype)')
0125
0126        code = self.end_module_code
0127        code.line('')
0128
0129    def end(self):
0130        self.expand_code.end_block()
0131
0132    def proc_stream(self, code):
0133        for (ev, item) in self.stream:
0134            if ev == START:
0135                if item.tag == Comment:
0136                    text = item.text.lstrip()
0137                    if text.startswith('!'):
0138                        continue
0139                    line = code.line
0140                    if text.startswith('<') or text.startswith('['):
0141                        sub = interpolate(item.text)
0142                        if isinstance(sub, list):
0143                            text = "''.join(%r)" % sub
0144                        else:
0145                            text = repr(sub)
0146                    else:
0147                        text = repr(item.text)
0148                    line('_e = Comment(%s)' % text)
0149                    line('yield (START, _e); yield (END, _e); del _e')
0150                elif item.tag == ProcessingInstruction:
0151                    if ' ' in item.text.strip():
0152                        (name, data) = item.text.split(' ', 1)
0153                    else:
0154                        (name, data) = (item.text, '')
0155                    if name in (KID_PI, KID_ALT_PI, KID_OLD_PI):
0156                        if data:
0157                            code.insert_block(data)
0158                    else:
0159                        c = self.depth and code or self.expand_code
0160                        c.line('_e = ProcessingInstruction(%r, %r)'                                     % (name, data) )
0162                        c.line('yield (START, _e); yield (END, _e); del _e')
0163                        del c
0164                else:
0165                    layout = None
0166                    if code is self.module_code:
0167                        layout = item.get(QNAME_LAYOUT)
0168                        if layout is not None:
0169                            del item.attrib[QNAME_LAYOUT]
0170                        decl = ['class Template(']
0171                        extends = item.get(QNAME_EXTENDS)
0172                        parts = []
0173                        if extends is not None:
0174                            del item.attrib[QNAME_EXTENDS]
0175                            for c in extends.split(','):
0176                                parts.append('template_util.get_base_class(%s, __file__)' % c)
0177                        parts.append('BaseTemplate')
0178                        decl.append(','.join(parts))
0179                        decl.append('):')
0180                        code = self.class_code
0181                        code.start_block(''.join(decl))
0182                        code.line('_match_templates = []')
0183                        code = self.expand_code
0184                        del decl, parts
0185                    self.def_proc(item, item.attrib, code)
0186                    if layout is not None:
0187                        old_code = code
0188                        code = CodeGenerator(level=1)
0189                        code.start_block("def _pull(self):")
0190                        code.line('exec template_util.get_locals(self, locals())')
0191                        code.line('kw = dict(layout_params)')
0192                        code.line('kw.update(dict([(name, getattr(self, name)) for name in _def_names]))')
0193                        code.line('kw.update(self.__dict__)')
0194                        # XXX hack: this could be avoided if template args were not stored in self.__dict__
0195                        # Note: these names cannot be passed to the layout template via layout_params
0196                        code.line('kw.pop("assume_encoding", None)')
0197                        code.line('kw.pop("_layout_classes", None)')
0198                        code.line('temp = template_util.get_base_class(%s, __file__)(**kw)' % layout)
0199                        code.line('temp._match_templates = self._match_templates + temp._match_templates')
0200                        code.line('return temp._pull()')
0201                        code.end_block()
0202                        self.inst_defs.append(code)
0203                        code = old_code
0204            elif ev == END and not item.tag in (ProcessingInstruction, Comment):
0205                break
0206            elif ev == TEXT:
0207                self.text_interpolate(item, code)
0208            elif ev == XML_DECL and item[1] is not None:
0209                self.module_code.line('encoding = %r' % item[1])
0210            elif ev == DOCTYPE:
0211                self.module_code.line('doctype = (%r, %r, %r)' % item)
0212
0213    def def_proc(self, item, attrib, code):
0214        attr_name = QNAME_DEF
0215        decl = attrib.get(attr_name)
0216        if decl is None:
0217            attr_name = QNAME_SLOT
0218            decl = attrib.get(attr_name)
0219        if decl is not None:
0220            del attrib[attr_name]
0221            old_code = code
0222            if '(' not in decl:
0223                decl = decl + '()'
0224            name, args = decl.split('(', 1)
0225            pos = args.rfind(')')
0226            args = args[0:pos].strip()
0227            self_ = args and 'self, ' or 'self'
0228            class_decl = '%s(%s%s)' % (name, self_, args)
0229
0230            # module function code
0231            code = CodeGenerator()
0232            code.start_block('def %s(*args, **kw):' % name)
0233            code.line('return Template().%s(*args, **kw)' % name)
0234            code.end_block()
0235            code.line('_def_names.append("%s")' % name)
0236            self.module_defs.append(code)
0237
0238            # instance method code
0239            code = CodeGenerator(level=1)
0240            code.start_block('def %s:' % class_decl)
0241            code.line('exec template_util.get_locals(self, locals())')
0242            code.line('current, ancestors = None, []')
0243            self.inst_defs.append(code)
0244            self.match_proc(item, attrib, code)
0245            code.end_block()
0246            if attr_name == QNAME_SLOT:
0247                old_code.line('for _e in template_util.generate_content(self.%s()): yield _e' % name)
0248        else:
0249            self.match_proc(item, attrib, code)
0250
0251    def match_proc(self, item, attrib, code):
0252        expr = attrib.get(QNAME_MATCH)
0253        if expr is not None:
0254            del attrib[QNAME_MATCH]
0255            old_code = code
0256            code = CodeGenerator(level=1)
0257            code.start_block('def _match_func(self, item, apply):')
0258            code.line('exec template_util.get_locals(self, locals())')
0259            code.line('current, ancestors = None, []')
0260            self.for_proc(item, attrib, code)
0261            code.end_block()
0262            code.line('_match_templates.append((lambda item: %s, _match_func))'                         % expr)
0264            self.inst_defs.append(code)
0265        else:
0266            self.for_proc(item, attrib, code)
0267
0268    def for_proc(self, item, attrib, code):
0269        expr = attrib.get(QNAME_FOR)
0270        if expr is not None:
0271            code.start_block('for %s:' % expr)
0272            del attrib[QNAME_FOR]
0273            self.if_proc(item, attrib, code)
0274            code.end_block()
0275        else:
0276            self.if_proc(item, attrib, code)
0277
0278    def if_proc(self, item, attrib, code):
0279        expr = attrib.get(QNAME_IF)
0280        if expr is not None:
0281            code.start_block('if %s:' % expr)
0282            del attrib[QNAME_IF]
0283            self.replace_proc(item, attrib, code)
0284            code.end_block()
0285        else:
0286            self.replace_proc(item, attrib, code)
0287
0288    def replace_proc(self, item, attrib, code):
0289        expr = attrib.get(QNAME_REPLACE)
0290        if expr is not None:
0291            del attrib[QNAME_REPLACE]
0292            attrib[QNAME_STRIP] = ""
0293            attrib[QNAME_CONTENT] = expr
0294        self.strip_proc(item, attrib, code)
0295
0296    def strip_proc(self, item, attrib, code):
0297        has_content = self.content_proc(item, attrib, code)
0298        expr, attr = (attrib.get(QNAME_STRIP), QNAME_STRIP)
0299        if expr is None:
0300            # XXX py:omit is deprecated equivalent of py:strip
0301            expr, attr = (attrib.get(QNAME_OMIT), QNAME_OMIT)
0302        start_block, end_block = (code.start_block, code.end_block)
0303        line = code.line
0304        if expr is not None:
0305            del attrib[attr]
0306            if expr != '':
0307                start_block("if not (%s):" % expr)
0308                self.attrib_proc(item, attrib, code)
0309                end_block()
0310            else:
0311                # element is always stripped
0312                pass
0313        else:
0314            self.attrib_proc(item, attrib, code)
0315        if has_content:
0316            code.start_block(
0317                'for _e in template_util.generate_content(_cont, current):')
0318            line('yield _e')
0319            line('del _e')
0320            code.end_block()
0321            # XXX should we use the hardcoded content if py:content is
0322            # specified but returns None?
0323            self.stream.eat()
0324        else:
0325            self.depth += 1
0326            self.proc_stream(code)
0327            self.depth -= 1
0328        if expr:
0329            start_block("if not (%s):" % expr)
0330            line('yield (END, current)')
0331            line('current = ancestors.pop(0)')
0332            end_block()
0333        elif expr != '':
0334            line('yield (END, current)')
0335            line('current = ancestors.pop(0)')
0336
0337    def attrib_proc(self, item, attrib, code):
0338        interp = 0
0339        line = code.line
0340        need_interpolation = 0
0341        names = namespaces(item, remove=1)
0342        for (k,v) in attrib.items():
0343            sub = interpolate(v)
0344            if id(sub) != id(v):
0345                attrib[k] = sub
0346                if isinstance(sub, list):
0347                    need_interpolation = 1
0348        expr = attrib.get(QNAME_ATTRIBUTES)
0349
0350        if expr is not None:
0351            del attrib[QNAME_ATTRIBUTES]
0352            attr_text = 'template_util.update_dict(%r, "%s", globals(), locals())'                   % (attrib, expr.replace('"', '\\\"'))
0354            attr_text = 'template_util.make_attrib(%s,self._get_assume_encoding())' % attr_text
0355        else:
0356            if attrib:
0357                if need_interpolation:
0358                    attr_text = 'template_util.make_attrib(%r,self._get_assume_encoding())' % attrib
0359                else:
0360                    attr_text = repr(attrib)
0361            else:
0362                attr_text = '{}'
0363        line('ancestors.insert(0, current)')
0364        line('current = Element(%r, %s)' % (item.tag, attr_text))
0365        if len(names):
0366            code.start_block('for _p, _u in %r.items():' % names)
0367            line('if not _u in omit_namespaces: yield (START_NS, (_p,_u))')
0368            code.end_block()
0369        line('yield (START, current)')
0370
0371    def content_proc(self, item, attrib, code):
0372        expr = attrib.get(QNAME_CONTENT)
0373        if expr is not None:
0374            del attrib[QNAME_CONTENT]
0375            code.line('_cont = %s' % expr)
0376            return 1
0377
0378    def text_interpolate(self, text, code):
0379        interp = 0
0380        line = code.line
0381        sub = interpolate(text)
0382        if isinstance(sub, list):
0383            code.start_block('for _e in %r:' % sub)
0384            code.line('for _e2 in template_util.generate_content(_e): yield _e2')
0385            code.end_block()
0386        else:
0387            line('yield (TEXT, %r)' % sub)
0388
0389class SubExpression(list):
0390    def __repr__(self):
0391        return "[%s]" % ', '.join(self)
0392
0393_sub_expr = re.compile(r"(?<!\$)\$\{(.+?)\}")
0394_sub_expr_short = re.compile(r"(?<!\$)\$([a-zA-Z][a-zA-Z0-9_\.]*)")
0395
0396def interpolate(text):
0397    parts = _sub_expr.split(text)
0398    if len(parts) == 1:
0399        parts = _sub_expr_short.split(text)
0400        if len(parts) == 1:
0401            return text.replace('$$', '$')
0402        else:
0403            last_checked = len(parts)
0404    else:
0405        last_checked = -1
0406    new_parts = SubExpression()
0407    i = 0
0408    while i < len(parts):
0409        part = parts[i]
0410        if (i % 2) == 1:
0411            # odd positioned elements are code
0412            new_parts.append(part)
0413        elif part:
0414            # even positioned elements are text
0415            if i >= last_checked:
0416                more_parts = _sub_expr_short.split(part)
0417                parts[i:i+1] = more_parts
0418                last_checked = i + len(more_parts)
0419                continue
0420            else:
0421                new_parts.append(repr(part.replace('$$', '$')))
0422        i += 1
0423    return new_parts
0424
0425
0426class CodeGenerator:
0427    """A simple Python code generator."""
0428
0429    level = 0
0430    tab = '\t'
0431
0432    def __init__(self, code=None, level=0, tab='\t'):
0433        self.code = code or []
0434        if level != self.level:
0435            self.level = level
0436        if tab != self.tab:
0437            self.tab = tab
0438
0439    def line(self, text):
0440        self.code.append('%s%s' % (self.tab * self.level, text))
0441
0442    def start_block(self, text):
0443        self.line(text)
0444        self.level+=1
0445
0446    def end_block(self, nblocks=1, with_pass=False):
0447        for n in range(nblocks):
0448            if with_pass:
0449                self.line('pass')
0450            self.level-=1
0451
0452    def insert_block(self, block):
0453        output_line = self.line
0454        lines = block.splitlines()
0455        if len(lines) == 1:
0456            # special case single lines
0457            output_line(lines[0].strip())
0458        else:
0459            # adjust the block
0460            for line in _adjust_python_block(lines, self.tab):
0461                output_line(line)
0462
0463    def __str__(self):
0464        self.code.append('')
0465        return '\n'.join(self.code)
0466
0467# Auxiliary function
0468
0469def _adjust_python_block(lines, tab='\t'):
0470    """Adjust the indentation of a Python block."""
0471    lines = [lines[0].strip()] + [line.rstrip() for line in lines[1:]]
0472    ind = None # find least index
0473    for line in lines[1:]:
0474        if line != '':
0475            s = line.lstrip()
0476            if s[0] != '#':
0477                i = len(line) - len(s)
0478                if ind is None or i < ind:
0479                    ind = i
0480                    if i == 0:
0481                        break
0482    if ind is not None or ind != 0: # remove indentation
0483        lines[1:] = [line[:ind].lstrip() + line[ind:]
0484            for line in lines[1:]]
0485    if lines[0] and not lines[0][0] == '#':
0486        # the first line contains code
0487        try: # try to compile it
0488            compile(lines[0], '<string>', 'exec')
0489            # if it works, line does not start new block
0490        except SyntaxError: # unexpected EOF while parsing?
0491            try: # try to compile the whole block
0492                block = '\n'.join(lines) + '\n'
0493                compile(block, '<string>', 'exec')
0494                # if it works, line does not start new block
0495            except IndentationError: # expected an indented block?
0496                # so try to add some indentation:
0497                lines2 = lines[:1] + [tab + line for line in lines[1:]]
0498                block = '\n'.join(lines2) + '\n'
0499                # try again to compile the whole block:
0500                compile(block, '<string>', 'exec')
0501                lines = lines2 # if it works, keep the indentation
0502            except:
0503                pass # leave it as it is
0504        except:
0505            pass # leave it as it is
0506    return lines
0507
0508# Python < 2.3 compatibility
0509try:
0510    enumerate
0511except NameError:
0512    def enumerate(seq):
0513        for i, elem in zip(range(len(seq)), seq):
0514            yield (i, elem)