Clone attributes as needed. Added Node.xpath, Node.match.
[tx] / nodes.py
1 # -*- coding:utf-8 -*-
2
3 # XML Nodes
4 # Copyright (C) 2005  Frédéric Jolliton <frederic@jolliton.com>
5
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA      02111-1307      USA
19
20 __all__ = [
21           'Node'
22         , 'Document'
23         , 'Element'
24         , 'Attribute'
25         , 'ProcessingInstruction'
26         , 'Comment'
27         , 'Text'
28 ]
29
30 import cStringIO as StringIO
31 import codecs
32 import time
33 import re
34
35 from error import Error, NodeError
36 from misc import shortenText, typeOf, stringValuesCompare
37
38 # IMPORTANT: Additionnal import at end of module
39
40 def someDuplicates( items , getValue = id ) :
41
42         '''Predicate to test if there is duplicate in 'items'.'''
43
44         values = set()
45         for item in items :
46                 value = getValue( item )
47                 if value in values :
48                         return True
49                 values.add( value )
50         return False
51
52 def _combineAdjacentText( nodes ) :
53
54         '''Combine adjacent text nodes.'''
55
56         lastTextNode = None
57         result = []
58         for node in nodes :
59                 if node.parent is not None :
60                         node = node.clone()
61                 if isinstance( node , Text ) :
62                         if node.content != '' :
63                                 if lastTextNode is None :
64                                         result.append( node )
65                                         lastTextNode = node
66                                 else :
67                                         lastTextNode.content += node.content
68                 else :
69                         result.append( node )
70                         lastTextNode = None
71         return tuple( result )
72
73 def _processAttributes( attributes ) :
74
75         '''Clone attributes that are already part of another tree (which have
76         a parent).'''
77
78         result = []
79         for attribute in attributes :
80                 if attribute.parent is not None :
81                         attribute = attribute.clone()
82                 result.append( attribute )
83         return tuple( result )
84
85 def _chainNodes( nodes , parent ) :
86
87         '''Update parent, prev and next reference for nodes in 'nodes'.  The
88         parent reference will point to 'parent'.'''
89
90         last = len( nodes ) - 1
91         for i , node in enumerate( nodes ) :
92                 node._setParent( parent )
93                 if i < last :
94                         node._setNext( nodes[ i + 1 ] )
95                 if i > 0 :
96                         node._setPrev( nodes[ i - 1 ] )
97
98 def xmlQuoteText( s ) :
99
100         '''Return string 's' with & and < characters translated to XML entity
101         references.'''
102
103         return s \
104                 .replace( '&' , '&amp;' ) \
105                 .replace( '<' , '&lt;' )
106
107 reCharacterToQuoteForText = re.compile( '[&<]' )
108 def streamXmlQuoteText( s , prt ) :
109
110         '''Feed string 's' to 'prt' with & and < characters translated to XML
111         entity references.'''
112
113         p = 0
114         while 1 :
115                 r = reCharacterToQuoteForText.search( s , p )
116                 if r is None :
117                         break
118                 e = r.start( 0 )
119                 prt( s[ p : e ] )
120                 p = r.end( 0 )
121                 if s[ e ] == '&' :
122                         prt( '&amp;' )
123                 elif s[ e ] == '<' :
124                         prt( '&lt;' )
125         prt( s[ p : ] )
126
127 def xmlQuoteAttribute( s ) :
128
129         '''Return string 's' with &, <, " and ' characters translated to XML
130         entity references.'''
131
132         return s \
133                 .replace( '&' , '&amp;' ) \
134                 .replace( '<' , '&lt;' ) \
135                 .replace( '"' , '&quot;' ) \
136                 .replace( "'" , '&apos;' )
137
138 reCharacterToQuoteForText = re.compile( '''[&<"']''' )
139 def streamXmlQuoteAttribute( s , prt ) :
140
141         '''Feed string 's' to 'prt' with &, <, " and ' characters translated
142         to XML entity references.'''
143
144         p = 0
145         while 1 :
146                 r = reCharacterToQuoteForText.search( s , p )
147                 if r is None :
148                         break
149                 e = r.start( 0 )
150                 prt( s[ p : e ] )
151                 p = r.end( 0 )
152                 if s[ e ] == '&' :
153                         prt( '&amp;' )
154                 elif s[ e ] == '<' :
155                         prt( '&lt;' )
156                 elif s[ e ] == '"' :
157                         prt( '&quot;' )
158                 elif s[ e ] == "'" :
159                         prt( '&apos;' )
160         prt( s[ p : ] )
161
162 #----------------------------------------------------------------------------
163
164 class DebugOutputContext( object ) :
165
166         '''Object to hold attributes when generating debug output of a XML
167         tree.'''
168
169         __slots__ = [
170                 'prt' , 'depth' , 'treeSpace' , 'prefix' ,
171                 'depthLimit' , 'nameLimit' , 'attributeLimit' ,
172                 'childrenLimit' , 'textLimit' , 'commentLimit' ]
173
174         def __init__( self ) :
175
176                 self.prt            = None
177                 self.depth          = 0
178                 self.treeSpace      = '  '
179                 self.prefix         = ''
180                 self.depthLimit     = None
181                 self.nameLimit      = None
182                 self.attributeLimit = None
183                 self.childrenLimit  = None
184                 self.textLimit      = None
185                 self.commentLimit   = None
186
187         def checkAttributeCount( self , count ) :
188
189                 '''Predicate to test if we can output 'count' attributes.'''
190
191                 return self.attributeLimit is None or count <= self.attributeLimit
192
193         def checkChildrenCount( self , count ) :
194
195                 '''Predicate to test if we can output 'count' children.'''
196
197                 return self.childrenLimit is None or count <= self.childrenLimit
198
199         def checkDepth( self , level = 1 ) :
200
201                 '''Predicate to test if at least 'level' level are still allowed
202                 before reaching depth limit.'''
203
204                 return self.depthLimit is None or self.depth + level <= self.depthLimit
205
206 #----------------------------------------------------------------------------
207
208 class Node( object ) :
209
210         __slots__ = [ 'root' , 'parent' , 'next' , 'prev' , 'position' , 'meta' ]
211
212         def __init__( self ) :
213
214                 self.root     = None  # Root node (document)
215                 self.parent   = None  # Parent node
216                 self.next     = None  # Next sibling node
217                 self.prev     = None  # Previous sibling node
218                 self.position = None  # Relative position in document tree
219                 self.meta     = None  # Place holder for misc data
220
221         def clone( self ) :
222
223                 '''Return a clone of the document (or subtree) 'self'.'''
224
225                 raise NotImplementedError
226
227         def iterStringValue( self ) :
228
229                 '''Iterator that list all the strings inside the node 'self'.'''
230
231                 raise NotImplementedError
232
233         def dmAttributes( self ) :
234
235                 raise NotImplementedError
236
237         def dmChildren( self ) :
238
239                 raise NotImplementedError
240
241         def dmDocumentUri( self ) :
242
243                 raise NotImplementedError
244
245         def dmNamespaceNodes( self ) :
246
247                 raise NotImplementedError
248
249         def dmNodeKind( self ) :
250
251                 raise NotImplementedError
252
253         def dmNodeName( self ) :
254
255                 raise NotImplementedError
256
257         def dmParent( self ) :
258
259                 raise NotImplementedError
260
261         def dmStringValue( self ) :
262
263                 raise NotImplementedError
264
265         def _setParent( self , node ) :
266
267                 if self.parent is not None and self.parent is not node :
268                         raise NodeError( 'parent reference is already set' )
269                 self.parent = node
270
271         def _setNext( self , node ) :
272
273                 if self.next is not None and self.next is not node :
274                         raise NodeError( 'next reference is already set' )
275                 self.next = node
276
277         def _setPrev( self , node ) :
278
279                 if self.prev is not None and self.prev is not node :
280                         raise NodeError( 'prev reference is already set' )
281                 self.prev = node
282
283         def xpath( self , path ) :
284
285                 import xpath
286                 xpathObject = xpath.XPath( path )
287                 return xpathObject.eval( self )
288
289         def __getitem__( self , path ) :
290
291                 if not isinstance( path , basestring ) :
292                         return super( Node , self ).__getitem__( path )
293                 else :
294                         return self.xpath( path )
295
296         # Abusing operator overloading
297         def __div__( self , path ) :
298
299                 return self.__getitem__( path )
300
301         def __idiv__( self , other ) :
302
303                 raise NotImplementedError
304
305         def match( self , pat ) :
306
307                 import pattern
308                 return pattern.test( self , pat )
309
310         def __cmp__( self , other ) :
311
312                 if other is self :
313                         return 0
314                 elif isinstance( other , Node ) :
315                         return stringValuesCompare( self.iterStringValue() ,
316                                                                                 other.iterStringValue() )
317                 else :
318                         return cmp( type( self ) , type( other ) )
319
320         def serialize( self , charset = 'utf8' , errors = 'strict' , file = None ) :
321
322                 if file is None :
323                         f = StringIO.StringIO()
324                 else :
325                         f = file
326                 encoder = codecs.getencoder( charset )
327                 prt = lambda s : f.write( encoder( s , errors )[ 0 ] )
328                 self._serialize( prt )
329                 if file is None :
330                         f.seek( 0 )
331                         return f.read()
332
333         def _serialize( self , prt ) :
334
335                 raise NotImplementedError
336
337         def asRxp( self ) :
338
339                 raise NotImplementedError
340
341         def asDebug( self , charset = 'utf8' , errors = 'strict' , file = None , prefix = '' ,
342                                  maxDepth = None , maxAttributes = None , maxChildren = None ) :
343
344                 if file is None :
345                         f = StringIO.StringIO()
346                 else :
347                         f = file
348                 encoder = codecs.getencoder( charset )
349                 prt = lambda s : f.write( encoder( s , errors )[ 0 ] )
350
351                 context = DebugOutputContext()
352                 context.prt            = prt
353                 context.prefix         = prefix
354                 context.depthLimit     = maxDepth
355                 context.attributeLimit = maxAttributes
356                 context.childrenLimit  = maxChildren
357                 context.nameLimit      = 24
358                 context.commentLimit   = 64
359                 context.textLimit      = 64
360
361                 if context.checkDepth( 1 ) :
362                         self._asDebug( context )
363
364                 if file is None :
365                         f.seek( 0 )
366                         return f.read()
367
368         def _asDebug( self , context ) :
369
370                 raise NotImplementedError
371
372         def __hash__( self ) :
373
374                 return id( self )
375
376 class Document( Node ) :
377
378         __slots__ = [ 'children' , 'documentUri' , 'numberOfNodes' , 'ids' , 'attributesByName' , 'elementsByName' ]
379
380         def __init__( self , children = () , finalize = True ) :
381
382                 super( Document , self ).__init__()
383
384                 for child in children :
385                         if not isinstance( child , ( Element , ProcessingInstruction , Comment , Text ) ) :
386                                 raise NodeError( '%s not allowed as child of Document' % typeOf( child ) )
387
388                 if someDuplicates( children , id ) :
389                         raise NodeError( 'Duplicate node is not allowed in an element' )
390                 children = _combineAdjacentText( children )
391
392                 self.children      = children
393                 self.documentUri   = None
394
395                 self.numberOfNodes = None
396
397                 self.ids                      = {}
398                 self.attributesByName = {}
399                 self.elementsByName   = {}
400
401                 _chainNodes( self.children , self )
402
403                 if finalize :
404                         self.finalize()
405
406         def clone( self ) :
407
408                 return Document( map( lambda child : child.clone() , self.children ) )
409
410         def iterStringValue( self ) :
411
412                 for node in iterDescendant( self ) :
413                         if isinstance( node , Text ) :
414                                 if node.content :
415                                         yield node.content
416
417         def dmAttributes( self ) :
418
419                 return ()
420
421         def dmChildren( self ) :
422
423                 return self.children
424
425         def dmDocumentUri( self ) :
426
427                 return self.documentUri or ()
428
429         def dmNamespaceNodes( self ) :
430
431                 return ()
432
433         def dmNodeKind( self ) :
434
435                 return 'document'
436
437         def dmNodeName( self ) :
438
439                 return ()
440
441         def dmParent( self ) :
442
443                 return ()
444
445         def dmStringValue( self ) :
446
447                 return ''.join( self.iterStringValue() )
448
449         def finalize( self , start = 0 ) :
450
451                 '''For every descendant of the document, update position, set root
452                 node, keep reference to elements with an 'id' attribute, keep
453                 references to all attributes and elements by name.'''
454
455                 idNames = ( 'id' , )
456                 self.ids = {} # for 'id' attribute
457                 self.attributesByName = {}
458                 self.elementsByName = {}
459                 pos = start
460                 t = time.time()
461                 for node in iterDescendantOrSelfFull( self ) :
462                         node.position = pos
463                         node.root = self
464                         pos += 1
465                         if isinstance( node , Attribute ) :
466                                 if node.name in idNames :
467                                         if node.name in self.ids :
468                                                 raise NodeError( 'Duplicate ID %r' % nade.name )
469                                         self.ids[ node.value ] = node.parent
470                                 self.attributesByName.setdefault( node.name , [] ).append( node )
471                         elif isinstance( node , Element ) :
472                                 self.elementsByName.setdefault( node.name , [] ).append( node )
473                 self.numberOfNodes = pos - start
474                 return pos
475
476         def _check( self ) :
477
478                 pass
479
480         def _serialize( self , prt ) :
481
482                 for children in self.children :
483                         children._serialize( prt )
484
485         def asRxp( self ) :
486
487                 return ( None , None , [ item.asRxp() for item in self.children ] , None )
488
489         def _asDebug( self , context ) :
490
491                 prefix = context.prefix + context.depth * context.treeSpace
492                 assert context.checkDepth( 1 ) , 'reached depth limit'
493                 context.prt( prefix )
494                 context.prt( 'DOCUMENT[%s] with %d nodes'
495                                          % ( self.position , self.numberOfNodes or -1 ) )
496                 context.prt( '\n' )
497                 #
498                 # Children
499                 #
500                 if self.children :
501                         if not context.checkDepth( 2 ) or context.childrenLimit == 0 :
502                                 context.prt( prefix )
503                                 context.prt( context.treeSpace )
504                                 context.prt( '[.. %d children ..]' % len( self.children ) )
505                                 context.prt( '\n' )
506                         elif context.checkChildrenCount( len( self.children ) ) :
507                                 context.depth += 1
508                                 for child in self.children :
509                                         child._asDebug( context )
510                                 context.depth -= 1
511                         else :
512                                 context.depth += 1
513                                 for child in self.children[ : context.childrenLimit ] :
514                                         child._asDebug( context )
515                                 context.depth -= 1
516                                 context.prt( prefix )
517                                 context.prt( context.treeSpace )
518                                 context.prt( '[.. and %d more children ..]'
519                                                          % ( len( self.children ) - context.childrenLimit ) )
520                                 context.prt( '\n' )
521
522         def __repr__( self ) :
523
524                 return '<Document with %d children>' % len( self.children )
525
526 class Element( Node ) :
527
528         __slots__ = [ 'name' , 'children' , 'attributes' ]
529
530         def __init__( self , name , attributes = () , children = () , finalize = False ) :
531
532                 super( Element , self ).__init__()
533
534                 for child in children :
535                         if not isinstance( child , ( Element , ProcessingInstruction , Comment , Text ) ) :
536                                 raise NodeError( '%s not allowed as child of Element' % typeOf( child ) )
537
538                 if someDuplicates( children , id ) :
539                         raise NodeError( 'Duplicate node is not allowed in an element' )
540                 children = _combineAdjacentText( children )
541
542                 names = set()
543                 for attribute in attributes :
544                         if not isinstance( attribute , Attribute ) :
545                                 raise NodeError( '%s not allowed as attribute of Element' % typeOf( attribute ) )
546                         if attribute.name in names :
547                                 raise NodeError( 'Duplicate attribute with name %r' % attribute.name )
548                         names.add( attribute.name )
549
550                 self.name = name
551                 self.children = children
552                 self.attributes = _processAttributes( attributes )
553
554                 _chainNodes( self.children , self )
555                 _chainNodes( self.attributes , self )
556
557                 if finalize :
558                         self.finalize()
559
560         def clone( self ) :
561
562                 return Element( self.name ,
563                                                 map( lambda attr : attr.clone() , self.attributes ) ,
564                                                 map( lambda child : child.clone() , self.children ) )
565
566         def getAttribute( self , name ) :
567
568                 for attribute in self.attributes :
569                         if attribute.name == name :
570                                 return attribute
571
572         def iterStringValue( self ) :
573
574                 for node in iterDescendant( self ) :
575                         if isinstance( node , Text ) :
576                                 if node.content :
577                                         yield node.content
578
579         def dmAttributes( self ) :
580
581                 return self.attributes
582
583         def dmChildren( self ) :
584
585                 return self.children
586
587         def dmDocumentUri( self ) :
588
589                 return ()
590
591         def dmNamespaceNodes( self ) :
592
593                 return ()
594
595         def dmNodeKind( self ) :
596
597                 return 'element'
598
599         def dmNodeName( self ) :
600
601                 return self.name
602
603         def dmParent( self ) :
604
605                 return self.parent or ()
606
607         def dmStringValue( self ) :
608
609                 return ''.join( self.iterStringValue() )
610
611         def finalize( self , start = 0 ) :
612
613                 '''For every descendant of the element, update position and set root
614                 node'''
615
616                 pos = start
617                 for node in iterDescendantOrSelfFull( self ) :
618                         node.position = pos
619                         node.root = self
620                         pos += 1
621                 return pos
622
623         def _serialize( self , prt ) :
624
625                 prt( '<' )
626                 prt( self.name )
627                 for attribute in self.attributes :
628                         prt( ' ' )
629                         attribute._serialize( prt )
630                 if not self.children :
631                         prt( '/>' )
632                 else :
633                         prt( '>' )
634                         for child in self.children :
635                                 child._serialize( prt )
636                         prt( '</' )
637                         prt( self.name )
638                         prt( '>' )
639
640         def asRxp( self ) :
641
642                 return ( self.name ,
643                                  dict( item.asRxp() for item in self.attributes ) or None ,
644                                  list( item.asRxp() for item in self.children ) or None ,
645                                  self.meta )
646
647         def _asDebug( self , context ) :
648
649                 prefix = context.prefix + context.depth * context.treeSpace
650                 assert context.checkDepth( 1 ) , 'reached depth limit'
651                 context.prt( prefix )
652                 context.prt( 'ELEMENT[%s] %s' % ( self.position , shortenText( self.name , context.nameLimit ) ) )
653                 context.prt( '\n' )
654                 #
655                 # Attributes
656                 #
657                 if self.attributes :
658                         if not context.checkDepth( 2 ) or context.attributeLimit == 0 :
659                                 context.prt( context.treeSpace )
660                                 context.prt( prefix )
661                                 context.prt( '[.. %d attributes ..]' % len( self.attributes ) )
662                                 context.prt( '\n' )
663                         elif context.checkAttributeCount( len( self.attributes ) ) :
664                                 context.depth += 1
665                                 for attribute in self.attributes :
666                                         attribute._asDebug( context )
667                                 context.depth -= 1
668                         else :
669                                 context.depth += 1
670                                 for attribute in self.attributes[ : context.attributeLimit ] :
671                                         attribute._asDebug( context )
672                                 context.depth -= 1
673                                 context.prt( prefix )
674                                 context.prt( context.treeSpace )
675                                 context.prt( '[.. and %d more attributes ..]'
676                                                          % ( len( self.attributes ) - context.attributeLimit ) )
677                                 context.prt( '\n' )
678                 #
679                 # Children
680                 #
681                 if self.children :
682                         if not context.checkDepth( 2 ) or context.childrenLimit == 0 :
683                                 context.prt( context.treeSpace )
684                                 context.prt( prefix )
685                                 context.prt( '[.. %d children ..]' % len( self.children ) )
686                                 context.prt( '\n' )
687                         elif context.checkChildrenCount( len( self.children ) ) :
688                                 context.depth += 1
689                                 for child in self.children :
690                                         child._asDebug( context )
691                                 context.depth -= 1
692                         else :
693                                 context.depth += 1
694                                 for child in self.children[ : context.childrenLimit ] :
695                                         child._asDebug( context )
696                                 context.depth -= 1
697                                 context.prt( prefix )
698                                 context.prt( context.treeSpace )
699                                 context.prt( '[.. and %d more children ..]'
700                                                          % ( len( self.children ) - context.childrenLimit ) )
701                                 context.prt( '\n' )
702
703         def __repr__( self ) :
704
705                 return '<Element %s with %d attributes and %d children>' \
706                         % ( shortenText( self.name , 24 ) , len( self.attributes ) , len( self.children ) )
707
708 class Attribute( Node ) :
709
710         __slots__ = [ 'name' , 'value' ]
711
712         def __init__( self , name , value ) :
713
714                 Node.__init__( self )
715                 if not isinstance( name , basestring ) :
716                         raise NodeError( '%s not allowed as name to construct Attribute' % typeOf( name ) )
717                 if not isinstance( value , basestring ) :
718                         raise NodeError( '%s not allowed as value to construct Attribute' % typeOf( value ) )
719                 self.name = name
720                 self.value = value
721
722         def clone( self ) :
723
724                 return Attribute( self.name , self.value )
725
726         def iterStringValue( self ) :
727
728                 yield self.value
729
730         def dmAttributes( self ) :
731
732                 return ()
733
734         def dmChildren( self ) :
735
736                 return ()
737
738         def dmDocumentUri( self ) :
739
740                 return ()
741
742         def dmNamespaceNodes( self ) :
743
744                 return ()
745
746         def dmNodeKind( self ) :
747
748                 return 'attribute'
749
750         def dmNodeName( self ) :
751
752                 return self.name
753
754         def dmParent( self ) :
755
756                 return self.parent or ()
757
758         def dmStringValue( self ) :
759
760                 return self.value
761
762         def _serialize( self , prt ) :
763
764                 prt( self.name )
765                 prt( '="' )
766                 streamXmlQuoteAttribute( self.value , prt )
767                 prt( '"' )
768
769         def asRxp( self ) :
770
771                 return ( self.name , self.value )
772
773         def _asDebug( self , context ) :
774
775                 prefix = context.prefix + context.depth * context.treeSpace
776                 assert context.checkDepth( 1 ) , 'reached depth limit'
777                 context.prt( prefix )
778                 context.prt( 'ATTRIBUTE[%s] %s = `%s`' \
779                                          % ( self.position ,
780                                                  shortenText( self.name , context.nameLimit ) ,
781                                                  shortenText( self.value , context.textLimit ) ) )
782                 context.prt( '\n' )
783
784         def __repr__( self ) :
785
786                 return '<Attribute %s>' % shortenText( self.name , 24 ) 
787
788 class ProcessingInstruction( Node ) :
789
790         __slots__ = [ 'target' , 'content' ]
791
792         def __init__( self , target , content = '' ) :
793
794                 super( ProcessingInstruction , self ).__init__()
795
796                 if target.lower() == 'xml' :
797                         raise NodeError( 'A processing instruction target cannot be named \'%s\'.' % ( target , ) )
798                 if '?>' in content :
799                         raise NodeError( "'?>' must not occur inside content of processing instruction." )
800                 self.target = target
801                 self.content = content
802
803         def iterStringValue( self ) :
804
805                 yield self.value
806
807         def dmAttributes( self ) :
808
809                 return ()
810
811         def dmChildren( self ) :
812
813                 return ()
814
815         def dmDocumentUri( self ) :
816                 
817                 return ()
818
819         def dmNamespaceNodes( self ) :
820
821                 return ()
822
823         def dmNodeKind( self ) :
824
825                 return 'processing-instruction'
826
827         def dmNodeName( self ) :
828
829                 return self.target
830
831         def dmParent( self ) :
832
833                 return self.parent or ()
834
835         def dmStringValue( self ) :
836
837                 return self.content
838
839         def _serialize( self , prt ) :
840
841                 assert self.target.lower() != 'xml' , '"xml" not allowed as PI target name'
842                 assert '?>' not in self.content , '"?>" not allowed inside PI contents'
843
844                 prt( '<?' )
845                 prt( self.target )
846                 if self.content :
847                         prt( ' ' )
848                         prt( self.content )
849                 prt( '?>' )
850
851         def _asDebug( self , context ) :
852
853                 prefix = context.prefix + context.depth * context.treeSpace
854                 assert context.checkDepth( 1 ) , 'reached depth limit'
855                 context.prt( prefix )
856                 context.prt( 'PI[%s] %s = `%s`'
857                                          % ( self.position ,
858                                                  shortenText( self.target , context.nameLimit ) ,
859                                                  shortenText( self.content , context.commentLimit ) ) )
860                 context.prt( '\n' )
861
862 class Comment( Node ) :
863
864         __slots__ = [ 'content' ]
865
866         def __init__( self , content ) :
867
868                 super( Comment , self ).__init__()
869
870                 if not isinstance( content , basestring ) :
871                         raise NodeError( '%s not allowed to construct Comment' % typeOf( content ) )
872                 if '--' in content :
873                         raise NodeError( "'--' at position %d is not allowed in comment" % content.index( '--' ) )
874                 if content.endswith( '-' ) :
875                         raise NodeError( "'-' must not occur at end of comment" )
876
877                 self.content = content
878
879         def iterStringValue( self ) :
880
881                 yield self.content
882
883         def dmAttributes( self ) :
884
885                 return ()
886
887         def dmChildren( self ) :
888
889                 return ()
890
891         def dmDocumentUri( self ) :
892
893                 return ()
894
895         def dmNamespaceNodes( self ) :
896
897                 return ()
898
899         def dmNodeKind( self ) :
900
901                 return 'comment'
902
903         def dmNodeName( self ) :
904
905                 return ()
906
907         def dmParent( self ) :
908
909                 return self.parent or ()
910
911         def dmStringValue( self ) :
912
913                 return self.content
914
915         def _serialize( self , prt ) :
916
917                 assert '--' not in self.content \
918                         and not self.content.endswith( '-' ) , \
919                         'invalid comment content'
920
921                 prt( '<!--' )
922                 prt( self.content )
923                 prt( '-->' )
924
925         def asRxp( self ) :
926
927                 # Not supported, use a raw element instead.
928                 return ( None , None , '<!--' + self.content.replace( '--' , '- -' ).replace( '--' , '- -' ) + '-->' )
929
930         def _asDebug( self , context ) :
931
932                 prefix = context.prefix + context.depth * context.treeSpace
933                 assert context.checkDepth( 1 ) , 'reached depth limit'
934                 context.prt( prefix )
935                 context.prt( 'COMMENT[%s] %r' % ( self.position , shortenText( self.content , context.commentLimit ) ) )
936                 context.prt( '\n' )
937
938         def __repr__( self ) :
939
940                 return '<Comment with %d characters>' % len( self.content )
941
942 class Text( Node ) :
943
944         __slots__ = [ 'content' ]
945
946         def __init__( self , content ) :
947
948                 super( Text , self ).__init__()
949
950                 if not isinstance( content , basestring ) :
951                         raise NodeError( '%s not allowed to construct Text' % typeOf( content ) )
952
953                 self.content = content
954
955         def clone( self ) :
956
957                 return Text( self.content )
958
959         def setParent( self , parent ) :
960
961                 if not self.content :
962                         raise NodeError( 'Text node inside document must not be empty' )
963                 return super( Text , self ).setParent( self , parent )
964
965         def iterStringValue( self ) :
966
967                 yield self.content
968
969         def dmAttributes( self ) :
970
971                 return ()
972
973         def dmChildren( self ) :
974
975                 return ()
976
977         def dmDocumentUri( self ) :
978
979                 return ()
980
981         def dmNamespaceNodes( self ) :
982
983                 return ()
984
985         def dmNodeKind( self ) :
986
987                 return 'text'
988
989         def dmNodeName( self ) :
990
991                 return ()
992
993         def dmParent( self ) :
994
995                 return self.parent or ()
996
997         def dmStringValue( self ) :
998
999                 return self.content
1000
1001         def _serialize( self , prt ) :
1002
1003                 streamXmlQuoteText( self.content , prt )
1004
1005         def asRxp( self ) :
1006
1007                 return self.content
1008
1009         def _asDebug( self , context ) :
1010
1011                 prefix = context.prefix + context.depth * context.treeSpace
1012                 assert context.checkDepth( 1 ) , 'reached depth limit'
1013                 context.prt( prefix )
1014                 context.prt( 'TEXT[%s] %r' % ( self.position , shortenText( self.content , context.textLimit ) ) )
1015                 context.prt( '\n' )
1016
1017         def __repr__( self ) :
1018
1019                 return '<Text with %d characters>' % len( self.content )
1020
1021 from iterators import iterDescendant, iterDescendantOrSelfFull
1022
1023 # Local Variables:
1024 # tab-width: 4
1025 # python-indent: 4
1026 # End: