Edgewall Software

Ticket #85: pysmatch.diff

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

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

  • genshi/template/tests/markup.py

     
    276276        finally:
    277277            shutil.rmtree(dirname)
    278278
     279    def test_basic_smatch(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('@name')}
     288                    </span>
     289                    <span>Some intervening text.</span>
     290                    <greeting name="Dude" />
     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_smatch_with_directives(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="//*[@name]" py:content="'Hello ' + str(select('@name'))" py:attrs="{'other':'greets'}" />
     314                    <span>Some intervening text.</span>
     315                    <greeting name="Dude" />
     316                </html>""")
     317            finally:
     318                file1.close()
     319
     320            loader = TemplateLoader([dirname])
     321            tmpl = loader.load('tmpl1.html')
     322            self.assertEqual("""<html>
     323                    <span>Some intervening text.</span>
     324                    <span other="greets">Hello Dude</span>
     325                </html>""", tmpl.generate().render())
     326        finally:
     327            shutil.rmtree(dirname)
     328
     329    def test_smatch_with_replace(self):
     330        dirname = tempfile.mkdtemp(suffix='genshi_test')
     331        try:
     332            file1 = open(os.path.join(dirname, 'tmpl1.html'), 'w')
     333            try:
     334                file1.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude"
     335                                     xmlns:py="http://genshi.edgewall.org/">
     336                    <span py:smatch="//*[@name]" py:replace="XML('&lt;span&gt;And now for something completely different.&lt;/span&gt;')" />
     337                    <span>Some intervening text.</span>
     338                    <greeting name="Dude" />
     339                </html>""")
     340            finally:
     341                file1.close()
     342
     343            loader = TemplateLoader([dirname])
     344            tmpl = loader.load('tmpl1.html')
     345            self.assertEqual("""<html>
     346                    <span>Some intervening text.</span>
     347                    <span>And now for something completely different.</span>
     348                </html>""", tmpl.generate(XML=XML).render())
     349        finally:
     350            shutil.rmtree(dirname)
     351
    279352    def test_fallback_when_include_found(self):
    280353        dirname = tempfile.mkdtemp(suffix='genshi_test')
    281354        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
     
    290294            else: # no matches
    291295                yield event
    292296
     297    def _smatch(self, stream, smatch_templates=None):
     298        """Internal stream filter that looks for and records any matches to py:smatch
     299           directives.
     300        """
     301        if smatch_templates is None:
     302            smatch_templates = []
    293303
     304        tail = []
     305        def _strip(stream):
     306            depth = 1
     307            while 1:
     308                event = stream.next()
     309                if event[0] is START:
     310                    depth += 1
     311                elif event[0] is END:
     312                    depth -= 1
     313                if depth > 0:
     314                    yield event
     315                else:
     316                    tail[:] = [event]
     317                    break
     318
     319        for kind, data, pos in stream:
     320            event = (kind, data, pos)
     321
     322            if kind is SUB:
     323                # look for SmatchDirectives
     324                directives, substream = data
     325                for idx, mt in enumerate(directives):
     326                    if not mt.__class__ is SmatchDirective:
     327                        continue
     328                    # process and remove SmatchDirective
     329                    smatch_templates.append((mt, directives[:idx] + directives[idx+1:], substream))
     330                    break
     331                else:
     332                    yield event
     333
     334            elif not kind is START and not kind is END:
     335                # We (currently) only care about start and end events for matching
     336                # We might care about namespace events in the future, though
     337                yield event
     338
     339            else:
     340                # look for matches
     341                for idx, (mt, mt_directives, mt_substream) in enumerate(smatch_templates):
     342                    if mt.test(event, mt.namespaces, None) is True:
     343                        # matched!
     344
     345                        # Let the remaining match templates know about the event so
     346                        # they get a chance to update their internal state
     347                        for othermt in [sm[0] for sm in smatch_templates[idx+1:]]:
     348                            othermt.test(event, othermt.namespaces, None, updateonly=True)
     349
     350                        # Consume and store all events until an end event
     351                        # corresponding to this start event is encountered
     352                        content = chain([event],
     353                                        self._smatch(_strip(stream),smatch_templates),
     354                                        tail)
     355                        content = list(content)
     356
     357                        # Let all match templates know about end event
     358                        for othermt in [sm[0] for sm in smatch_templates]:
     359                            othermt.test(tail[0], othermt.namespaces, None, updateonly=True)
     360
     361                        # Create the select substream for accessing parts of
     362                        # the matched element / substream.
     363                        select_substream = Stream(content)
     364
     365                        staticmatch = StaticMatchDirective(mt_substream,mt_directives,select_substream,mt.namespaces)
     366                        yield (SUB, ([staticmatch], select_substream), pos)
     367
     368                        break
     369                else:
     370                    yield (kind, data, pos)
     371
     372
     373
    294374INCLUDE = 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