Edgewall Software

Ticket #85: pysmatch.2.diff

File pysmatch.2.diff, 13.2 KB (added by hodgestar+genshi@…, 17 years ago)

Patch against http://svn.edgewall.org/repos/genshi/trunk r466 which implements a py:smatch which works with subprograms.

  • genshi/path.py

     
    3636
    3737from genshi.core import Stream, Attrs, Namespace, QName
    3838from genshi.core import START, END, TEXT, COMMENT, PI
     39from genshi.template.core import EXPR
    3940
    4041__all__ = ['Path', 'PathSyntaxError']
    4142
     
    620621    """Node test that matches any text event."""
    621622    __slots__ = []
    622623    def __call__(self, kind, data, pos, namespaces, variables):
    623         return kind is TEXT
     624        return kind is TEXT or kind is EXPR
    624625    def __repr__(self):
    625626        return 'text()'
    626627
  • genshi/template/tests/markup.py

     
    276276        finally:
    277277            shutil.rmtree(dirname)
    278278
     279    def test_smatch_expr_text(self):
     280        dirname = tempfile.mkdtemp(suffix='genshi_test')
     281        try:
     282            file1 = open(os.path.join(dirname, 'tmpl1.html'), 'w')
     283            try:
     284                file1.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude"
     285                                     xmlns:py="http://genshi.edgewall.org/">
     286                    <span py:smatch="greeting">
     287                        Hello ${select('./text()')}
     288                    </span>
     289                    <span>Some intervening text.</span>
     290                    <greeting>${'Dude'}</greeting>
     291                </html>""")
     292            finally:
     293                file1.close()
     294
     295            loader = TemplateLoader([dirname])
     296            tmpl = loader.load('tmpl1.html')
     297            self.assertEqual("""<html>
     298                    <span>Some intervening text.</span>
     299                    <span>
     300                        Hello Dude
     301                    </span>
     302                </html>""", tmpl.generate().render())
     303        finally:
     304            shutil.rmtree(dirname)
     305
     306    def test_basic_smatch(self):
     307        dirname = tempfile.mkdtemp(suffix='genshi_test')
     308        try:
     309            file1 = open(os.path.join(dirname, 'tmpl1.html'), 'w')
     310            try:
     311                file1.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude"
     312                                     xmlns:py="http://genshi.edgewall.org/">
     313                    <span py:smatch="greeting">
     314                        Hello ${select('@name')}
     315                    </span>
     316                    <span>Some intervening text.</span>
     317                    <greeting name="Dude" />
     318                </html>""")
     319            finally:
     320                file1.close()
     321
     322            loader = TemplateLoader([dirname])
     323            tmpl = loader.load('tmpl1.html')
     324            self.assertEqual("""<html>
     325                    <span>Some intervening text.</span>
     326                    <span>
     327                        Hello Dude
     328                    </span>
     329                </html>""", tmpl.generate().render())
     330        finally:
     331            shutil.rmtree(dirname)
     332
     333    def test_smatch_with_directives(self):
     334        dirname = tempfile.mkdtemp(suffix='genshi_test')
     335        try:
     336            file1 = open(os.path.join(dirname, 'tmpl1.html'), 'w')
     337            try:
     338                file1.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude"
     339                                     xmlns:py="http://genshi.edgewall.org/">
     340                    <span py:smatch="//*[@name]" py:content="'Hello ' + str(select('@name'))" py:attrs="{'other':'greets'}" />
     341                    <span>Some intervening text.</span>
     342                    <greeting name="Dude" py:content="foo" py:attrs="{'style':'foo'}"/>
     343                </html>""")
     344            finally:
     345                file1.close()
     346
     347            loader = TemplateLoader([dirname])
     348            tmpl = loader.load('tmpl1.html')
     349            self.assertEqual("""<html>
     350                    <span>Some intervening text.</span>
     351                    <span other="greets">Hello Dude</span>
     352                </html>""", tmpl.generate().render())
     353        finally:
     354            shutil.rmtree(dirname)
     355
     356    def test_smatch_with_replace(self):
     357        dirname = tempfile.mkdtemp(suffix='genshi_test')
     358        try:
     359            file1 = open(os.path.join(dirname, 'tmpl1.html'), 'w')
     360            try:
     361                file1.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude"
     362                                     xmlns:py="http://genshi.edgewall.org/">
     363                    <span py:smatch="//*[@name]" py:replace="XML('&lt;span&gt;And now for something completely different.&lt;/span&gt;')" />
     364                    <span>Some intervening text.</span>
     365                    <greeting name="Dude" />
     366                </html>""")
     367            finally:
     368                file1.close()
     369
     370            loader = TemplateLoader([dirname])
     371            tmpl = loader.load('tmpl1.html')
     372            self.assertEqual("""<html>
     373                    <span>Some intervening text.</span>
     374                    <span>And now for something completely different.</span>
     375                </html>""", tmpl.generate(XML=XML).render())
     376        finally:
     377            shutil.rmtree(dirname)
     378
    279379    def test_fallback_when_include_found(self):
    280380        dirname = tempfile.mkdtemp(suffix='genshi_test')
    281381        try:
  • genshi/template/markup.py

     
    4242
    4343    directives = [('def', DefDirective),
    4444                  ('match', MatchDirective),
     45                  ('smatch', SmatchDirective),
    4546                  ('when', WhenDirective),
    4647                  ('otherwise', OtherwiseDirective),
    4748                  ('for', ForDirective),
     
    185186        return streams[0]
    186187
    187188    def _prepare(self, stream):
    188         for kind, data, pos in Template._prepare(self, stream):
     189        for kind, data, pos in self._smatch(Template._prepare(self, stream)):
    189190            if kind is INCLUDE:
    190191                data = data[0], list(self._prepare(data[1]))
    191192            yield kind, data, pos
     
    290291            else: # no matches
    291292                yield event
    292293
     294    def _smatch(self, stream, smatch_templates=None, smatch_directives=[]):
     295        """Internal stream filter that looks for and records any matches to py:smatch
     296           directives.
     297        """
     298        stream = iter(stream)
     299        if smatch_templates is None:
     300            smatch_templates = []
    293301
     302        tail = []
     303        def _strip(stream):
     304            depth = 1
     305            while 1:
     306                event = stream.next()
     307                if event[0] is START:
     308                    depth += 1
     309                elif event[0] is END:
     310                    depth -= 1
     311                if depth > 0:
     312                    yield event
     313                else:
     314                    tail[:] = [event]
     315                    break
     316
     317        for kind, data, pos in stream:
     318            event = (kind, data, pos)
     319
     320            if kind is SUB:
     321                # look for SmatchDirectives
     322                directives, substream = data
     323                for idx, mt in enumerate(directives):
     324                    if not mt.__class__ is SmatchDirective:
     325                        continue
     326                    # process and remove SmatchDirective
     327                    smatch_templates.append((mt, directives[:idx] + directives[idx+1:], substream))
     328                    break
     329                else:
     330                    # Process substream looking for matches and new match templates
     331                    newsubstream = list(self._smatch(substream, smatch_templates, directives))
     332                    if substream[0][0] != newsubstream[0][0]:
     333                        # there's been a match at the top, directives will have been combined
     334                        # into the new sub.
     335                        for event in newsubstream:
     336                            yield event
     337                    else:
     338                        yield (SUB, (directives, newsubstream), pos)
     339
     340            elif not kind is START and not kind is END:
     341                # We (currently) only care about start and end events for matching
     342                # We might care about namespace events in the future, though
     343                yield event
     344
     345            else:
     346                # look for matches
     347                for idx, (mt, mt_directives, mt_substream) in enumerate(smatch_templates):
     348                    if mt.test(event, mt.namespaces, None) is True:
     349                        # matched!
     350
     351                        # Let the remaining match templates know about the event so
     352                        # they get a chance to update their internal state
     353                        for othermt in [sm[0] for sm in smatch_templates[idx+1:]]:
     354                            othermt.test(event, othermt.namespaces, None, updateonly=True)
     355
     356                        # Consume and store all events until an end event
     357                        # corresponding to this start event is encountered
     358                        content = chain([event],
     359                                        self._smatch(_strip(stream),smatch_templates),
     360                                        tail)
     361                        content = list(content)
     362
     363                        # Let all match templates know about end event
     364                        for othermt in [sm[0] for sm in smatch_templates]:
     365                            othermt.test(tail[0], othermt.namespaces, None, updateonly=True)
     366
     367                        # Create the select substream for accessing parts of
     368                        # the matched element / substream.
     369                        select_substream = Stream(content)
     370
     371                        staticmatch = StaticMatchDirective(mt_substream,mt_directives,select_substream,mt.namespaces)
     372                        yield (SUB, (smatch_directives+[staticmatch], select_substream), pos) # apply staticmatch last
     373
     374                        break
     375                else:
     376                    yield (kind, data, pos)
     377
     378
     379
    294380INCLUDE = MarkupTemplate.INCLUDE
  • genshi/template/directives.py

     
    2323
    2424__all__ = ['AttrsDirective', 'ChooseDirective', 'ContentDirective',
    2525           'DefDirective', 'ForDirective', 'IfDirective', 'MatchDirective',
    26            'OtherwiseDirective', 'ReplaceDirective', 'StripDirective',
    27            'WhenDirective', 'WithDirective']
     26           'OtherwiseDirective', 'ReplaceDirective',
     27           'SmatchDirective','StaticMatchDirective',
     28           'StripDirective','WhenDirective', 'WithDirective']
    2829
    2930
    3031class DirectiveMeta(type):
     
    447448    attach = classmethod(attach)
    448449
    449450
     451class SmatchDirective(Directive):
     452    """Implementation of the `py:smatch` template directive.
     453   
     454       The syntax for using py:smatch is the same as that for py:match.
     455       The py:smatch XPath query is performed immediately after the
     456       templated is parsed whereas the py:match query is performed when
     457       the template is generated/rendered. In both cases the replacement
     458       markup is only created on template generation.
     459
     460       py:smatch avoids performing XPath matches when the template is
     461       generated at the cost of not being able to access the context in
     462       the XPath expression or match XML not present in the template file
     463       (for example, attributes generated by py:attr).
     464    """
     465    __slots__ = ['path','namespaces','test']
     466
     467    ATTRIBUTE = 'path'
     468
     469    def __init__(self, value, namespaces=None, filename=None, lineno=-1,
     470                 offset=-1):
     471        Directive.__init__(self, None, namespaces, filename, lineno, offset)
     472        self.path = Path(value, filename, lineno)
     473        self.test = self.path.test(ignore_context=True)
     474        self.namespaces = namespaces or {}
     475
     476    def __call__(self, stream, ctxt, directives):
     477        raise RuntimeError("All SmatchDirectives should have been " \
     478               "removed from the stream by the time directives are called.")
     479
     480    def __repr__(self):
     481        return '<%s "%s">' % (self.__class__.__name__, self.path.source)
     482
     483
     484class StaticMatchDirective(object):
     485    """After a template has been parsed, a filter is run to replace
     486       all SmatchDirectives with StaticMatchDirectives at the point
     487       where there XPath expressions match the stream.
     488       
     489       This is an unregistered directive.
     490    """
     491    __slots__ = ['smatch_substream','smatch_directives','select_substream','namespaces']
     492
     493    def __init__(self, smatch_substream, smatch_directives, select_substream, namespaces):
     494        self.select_substream = select_substream
     495        self.smatch_substream = smatch_substream
     496        self.smatch_directives = smatch_directives
     497        self.namespaces = namespaces
     498
     499    def __call__(self, stream, ctxt, directives):
     500        def select(path):
     501            return self.select_substream.select(path, self.namespaces, ctxt)
     502
     503        ctxt.push(dict(select=select))
     504        for event in _apply_directives(self.smatch_substream, ctxt, self.smatch_directives):
     505            yield event
     506        ctxt.pop()
     507
     508
    450509class StripDirective(Directive):
    451510    """Implementation of the `py:strip` template directive.
    452511