Edgewall Software

Ticket #85: pysmatch.diff

File pysmatch.diff, 10.8 KB (added by hodgestar+genshi@…, 5 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