Updated HTMLParser to recognize XML element name and attribute name.
[tx] / htmlparser.py
1 # -*- coding: utf-8 -*-
2
3 # htmlparser.py - An error tolerant HTML parser.
4 # Copyright (C) 2004,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 #
21 # This module can replace HTMLParser Python parser.
22 #
23 # It never throw an exception even for worst HTML document.
24 # However, it is not able to parse SGML declaration statement
25 # with complex syntax.
26 #
27
28 #
29 # TODO:
30 #
31 # [ ] For incremental parsing, keep a pointer to the maximum position
32 #     from which we should start again to match (for example, when
33 #     looking for </script>, we search </. We can remember where the
34 #     latest </ was found and start again from this point next time
35 #     new characters are added.)
36 #
37
38 __all__ = [ 'HTMLParser' , 'HTMLParseError' ]
39
40 import re
41 from htmlentitydefs import entitydefs
42
43 from misc import shortenText
44
45 reEndOfData = re.compile(
46         '[&<]'  # Either & or <
47 )
48 reCheckTag = re.compile(
49         '/?[a-z]' , re.I
50 )
51 reTagName = re.compile(
52         '^[a-z_:][a-z0-9._:-]*' , re.I
53 )
54 reEndOfTag = re.compile( # <CHAR*> (<QUOTED> <CHAR*>)* '>'
55         '[^\'">]*' # ..
56         '(?:'
57         '(?:'
58         r"'[^']*'" # double-quote string
59         '|'
60         r'"[^"]*"' # single-quote string
61         ')'
62         '[^\'">]*' # ..
63         ')*'
64         '>'
65 )
66 reAttr = re.compile(
67         '([a-z_:][a-z0-9._:-]*)' # name
68         '('
69         r'\s*=\s*'         # spaces then =
70         '('
71         '[^\'"\\s]+'       # anything but spaces and quote characters
72         '|'
73         '"[^"]*"'          # double-quote string
74         '|'
75         "'[^']*'"          # single-quote string
76         ')?'
77         ')?' , re.I
78 )
79 reEntity = re.compile(
80         '&'
81         '(?:'
82         r'#(\d+|x[\da-f]+)' # numeric entity (&#160; or &#xA0;)
83         '|'
84         '([a-z]+)'          # named entity (&nbsp;)
85         ')'
86         ';?' , re.I
87 )
88
89 # Not used. Only for compatibility with HTMLParser python module.
90 class HTMLParseError : pass
91
92 defaultEntities = {
93         'amp'  : '&' ,
94         'lt'   : '<' ,
95         'gt'   : '>' ,
96         'quot' : '"' ,
97         'apos' : "'"
98 }
99
100 def _decodeAttr( s ) :
101
102         '''
103         >>> _decodeAttr( 'bar&apos;s &amp; baz &#256;' )
104         u"bar's & baz \u0100"
105         '''
106
107         r = reEntity.search( s )
108         if r is None :
109                 return s
110         else :
111                 result = []
112                 p = 0
113                 while r is not None :
114                         result.append( s[ p : r.start( 0 ) ] )
115                         p = r.end( 0 )
116                         if r.group( 1 ) is not None :
117                                 try :
118                                         result.append( unichr( int( r.group( 1 ) ) ) )
119                                 except OverflowError :
120                                         result.append( r.group( 0 ) )
121                         else :
122                                 e = defaultEntities.get( r.group( 2 ).lower() )
123                                 if e is None :
124                                         result.append( r.group( 0 ) )
125                                 else :
126                                         result.append( e )
127                         r = reEntity.search( s , p )
128                 result.append( s[ p : ] )
129                 return ''.join( result )
130
131 def _parseAttr( s ) :
132
133         '''
134         >>> _parseAttr( 'foo bar=baz x="y" z=\'quux\'' )
135         [('foo', None), ('bar', 'baz'), ('x', 'y'), ('z', 'quux')]
136         '''
137
138         attrs = []
139         p = 0
140         while p < len( s ) :
141                 r = reAttr.search( s , p )
142                 if r is None :
143                         break
144                 k , eq , v = r.groups()
145                 if v :
146                         if v[ 0 ] == "'" or v[ 0 ] == '"' :
147                                 v = v[ 1 : -1 ]
148                         v = _decodeAttr( v )
149                 elif eq :
150                         v = ''
151                 attrs.append( ( k , v ) )
152                 p = r.end( 0 )
153         return attrs
154
155 class HTMLParser( object ) :
156
157         __slots__ = [ '__buffer' , '__pos' , '__cdataTags' , '__waitTag' ]
158
159         def __init__( self ) :
160
161                 self.__buffer = ''
162                 self.__pos = 0
163                 #self.__restart = 0
164                 #
165                 # Tags with CDATA type
166                 #
167                 self.__cdataTags = set( ( 'script' , 'style' ) )
168                 self.__waitTag = None
169
170         def feed( self , s ) :
171
172                 self.__buffer = self.__buffer[ self.__pos : ] + s
173                 self.__pos = 0
174                 self.__process()
175
176         def close( self ) :
177
178                 self.__process( finalize = True )
179
180         def handle_data( self , data ) : pass
181         def handle_starttag( self , name , attr ) :     pass
182         def handle_endtag( self , name ) : pass
183         def handle_charref( self , name ) :     pass
184         def handle_entityref( self , name ) : pass
185         def handle_comment( self , data ) :     pass
186         def handle_decl( self , data ) : pass
187         def handle_pi( self , data ) : pass
188         def handle_startendtag( self , name , attr ) :
189
190                 self.handle_starttag( name , attr )
191                 self.handle_endtag( name )
192
193         def getpos( self ) :
194
195                 return ( 0 , 0 )
196
197         def __process( self , finalize = False ) :
198
199                 #
200                 # 1-letter variable used here:
201                 #
202                 # b = buffer
203                 # p = current position
204                 # e,f = end markers
205                 # r = regex result
206                 # s = size
207                 #
208                 b , p = self.__buffer , self.__pos
209                 if self.__waitTag :
210                         wt = self.__waitTag.lower()
211                         e = p
212                         while 1 :
213                                 e = b.find( '</' , e )
214                                 if e == -1 :
215                                         break
216                                 if b[ e + 2 : e + 2 + len( wt ) ].lower() == wt :
217                                         self.handle_data( b[ p : e ] )
218                                         p = e
219                                         self.__waitTag = None
220                                         break
221                                 e += 2
222                 else :
223                         while p < len( b ) :
224                                 #print '%4d' % p , shortenText( b[ p : ] , 30 )
225                                 if b[ p ] == '<' :
226                                         if b.startswith( '<?' , p ) :
227                                                 e = b.find( '?>' , p + 2 )
228                                                 if e == -1 :
229                                                         break
230                                                 else :
231                                                         pi = b[ p + 2 : e ]
232                                                 p = e + 2
233                                                 self.handle_pi( pi )
234                                         elif not b.startswith( '<!' , p ) :
235                                                 r = reEndOfTag.match( b , p + 1 )
236                                                 if r is None :
237                                                         break
238                                                 e = r.end( 0 )
239                                                 tag = b[ p : e ]
240                                                 rn = reCheckTag.match( tag , 1 )
241                                                 if rn is None :
242                                                         self.handle_data( b[ p ] )
243                                                         p += 1
244                                                 else :
245                                                         self.__processTag( tag )
246                                                         p = e
247                                                         if self.__waitTag :
248                                                                 wt = self.__waitTag.lower()
249                                                                 e = p
250                                                                 while 1 :
251                                                                         e = b.find( '</' , e )
252                                                                         if e == -1 :
253                                                                                 break
254                                                                         if b[ e + 2 : e + 2 + len( wt ) ].lower() == wt :
255                                                                                 self.handle_data( b[ p : e ] )
256                                                                                 p = e
257                                                                                 self.__waitTag = None
258                                                                                 break
259                                                                         e += 2
260                                                                 if e == -1 :
261                                                                         break
262                                         elif b.startswith( '<![CDATA[' , p ) :
263                                                 e = b.find( ']]>' , p + 9 )
264                                                 if e == -1 :
265                                                         break
266                                                 else :
267                                                         cdata = b[ p + 9 : e ]
268                                                         p = e + 3
269                                                 self.handle_data( cdata )
270                                         elif b.startswith( '<!--' , p ) :
271                                                 e , s = b.find( '-->' , p + 4 ) , 3
272                                                 if e == -1 :
273                                                         e , s = b.find( '->' , p + 4 ) , 2
274                                                 if e == -1 :
275                                                         e , s = b.find( '>' , p + 4 ) , 1
276                                                 if e == -1 : # Unterminated comment
277                                                         break
278                                                 else :
279                                                         comment = b[ p + 4 : e ]
280                                                         p = e + s
281                                                 self.handle_comment( comment )
282                                         else : # b.startswith( '<!' )
283                                                 # Discard it.
284                                                 e = b.find( '>' , p + 2 ) # We only handle "simple" declaration.
285                                                 if e == -1 :
286                                                         break
287                                                 else :
288                                                         self.handle_decl( b[ p + 2 : e ] )
289                                                         p = e + 1
290                                 elif b[ p ] == '&' :
291                                         r = reEntity.match( b , p )
292                                         if r is None :
293                                                 if len( b ) - p > 3 :
294                                                         self.handle_data( '&' )
295                                                         p += 1
296                                                 else :
297                                                         break
298                                         else :
299                                                 if r.group( 1 ) is not None :
300                                                         ref = r.group( 1 )
301                                                         if not finalize and not ref.endswith( ';' ) and r.end( 0 ) == len( b ) :
302                                                                 break
303                                                         self.handle_charref( ref )
304                                                 else :
305                                                         ref = r.group( 2 )
306                                                         if not finalize and not ref.endswith( ';' ) and r.end( 0 ) == len( b ) :
307                                                                 break
308                                                         self.handle_entityref( ref )
309                                                 p = r.end( 0 )
310                                 else :
311                                         r = reEndOfData.search( b , p )
312                                         if r is None :
313                                                 if not finalize :
314                                                         break # wait for end of data
315                                                 data = b[ p : ]
316                                                 p = len( b )
317                                         else :
318                                                 e = r.start( 0 )
319                                                 data = b[ p : e ]
320                                                 p = e
321                                         self.handle_data( data )
322                 if finalize :
323                         if p < len( b ) :
324                                 self.handle_data( b[ p : ] )
325                                 p = len( b )
326                 self.__buffer , self.__pos = b , p
327
328         def __processTag( self , tag ) :
329
330                 if tag.startswith( '<!' ) :
331                         self.handle_decl( tag[ 2 : -1 ] )
332                 else :
333                         tagContents = tag[ 1 : -1 ]
334                         tagType = 0 # 0: start, 1: end, 2: empty
335                         if tagContents.startswith( '/' ) :
336                                 tagType = 1
337                                 tagContents = tagContents[ 1 : ]
338                         elif tagContents.endswith( '/' ) : # and ' ' not in tagContents :
339                                 tagType = 2
340                                 tagContents = tagContents[ : -1 ]
341                         r = reTagName.match( tagContents )
342                         if r is not None :
343                                 e = r.end( 0 )
344                                 name , attr = tagContents[ : e ] , tagContents[ e : ]
345                                 attr = _parseAttr( attr )
346                                 if tagType == 0 :
347                                         self.handle_starttag( name , attr )
348                                         name = name.lower()
349                                         if name in self.__cdataTags :
350                                                 self.__waitTag = name # Start of CDATA element
351                                 elif tagType == 1 :
352                                         self.handle_endtag( name )
353                                 elif tagType == 2 :
354                                         self.handle_startendtag( name , attr )
355                                 else :
356                                         raise HTMLParser
357                         else :
358                                 self.handle_data( tag )
359
360 class HTMLParserDebug( HTMLParser ) :
361
362         def handle_data( self , data ) :
363
364                 print 'data(%r)' % data
365
366         def handle_starttag( self , name , attr ) :
367
368                 print 'starttag(%r,%r)' % ( name , attr )
369
370         def handle_endtag( self , name ) :
371
372                 print 'endtag(%r)' % name
373
374         def handle_startendtag( self , name , attr ) :
375
376                 print 'startendtag(%r,%r)...' % ( name , attr )
377                 HTMLParser.handle_startendtag( self , name , attr )
378
379         def handle_charref( self , name ) :
380
381                 print 'charref(%r)' % name
382
383         def handle_entityref( self , name ) :
384
385                 print 'entityref(%r)' % name
386
387         def handle_comment( self , data ) :
388
389                 print 'comment(%r)' % data
390
391         def handle_decl( self , data ) :
392
393                 print 'decl(%r)' % data
394
395         def handle_pi( self , data ) :
396
397                 print 'pi(%r)' % data
398
399 # Local Variables:
400 # tab-width: 4
401 # python-indent: 4
402 # End: