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