initial import
[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 , v = r.groups()
135                 if v :
136                         if v[ 0 ] == "'" or v[ 0 ] == '"' :
137                                 v = v[ 1 : -1 ]
138                         v = _decodeAttr( v )
139                 attrs.append( ( k , v ) )
140                 p = r.end( 0 )
141         return attrs
142
143 class HTMLParser( object ) :
144
145         __slots__ = [ '__buffer' , '__pos' , '__cdataTags' , '__waitTag' ]
146
147         def __init__( self ) :
148
149                 self.__buffer = ''
150                 self.__pos = 0
151                 #
152                 # Tags with CDATA type
153                 #
154                 self.__cdataTags = set( ( 'script' , 'style' ) )
155                 self.__waitTag = None
156
157         def feed( self , s ) :
158
159                 self.__buffer = self.__buffer[ self.__pos : ] + s
160                 self.__pos = 0
161                 self.__process()
162
163         def close( self ) :
164
165                 self.__process( finalize = True )
166
167         def handle_data( self , data ) : pass
168         def handle_starttag( self , name , attr ) :     pass
169         def handle_endtag( self , name ) : pass
170         def handle_charref( self , name ) :     pass
171         def handle_entityref( self , name ) : pass
172         def handle_comment( self , data ) :     pass
173         def handle_decl( self , data ) : pass
174         def handle_pi( self , data ) : pass
175         def handle_startendtag( self , name , attr ) :
176
177                 self.handle_starttag( name , attr )
178                 self.handle_endtag( name )
179
180         def getpos( self ) :
181
182                 return ( 0 , 0 )
183
184         def __process( self , finalize = False ) :
185
186                 #
187                 # 1-letter variable used here:
188                 #
189                 # b = buffer
190                 # p = current position
191                 # e,f = end markers
192                 # r = regex result
193                 # s = size
194                 #
195                 b , p = self.__buffer , self.__pos
196                 if self.__waitTag :
197                         e = b.find( '</' , p )
198                         if e != -1 :
199                                 self.handle_data( b[ p : e ] )
200                                 p = e
201                                 self.__waitTag = None
202                 else :
203                         while p < len( b ) :
204                                 #print '%4d' % p , shortenText( b[ p : ] , 30 )
205                                 if b[ p ] == '<' :
206                                         if b.startswith( '<?' , p ) :
207                                                 e = b.find( '?>' , p + 2 )
208                                                 if e == -1 :
209                                                         break
210                                                 else :
211                                                         pi = b[ p + 2 : e ]
212                                                 p = e + 2
213                                                 self.handle_pi( pi )
214                                         elif not b.startswith( '<!' , p ) :
215                                                 r = reEndOfTag.match( b , p + 1 )
216                                                 if r is None :
217                                                         break
218                                                 e = r.end( 0 )
219                                                 tag = b[ p : e ]
220                                                 rn = reCheckTag.match( tag , 1 )
221                                                 if rn is None :
222                                                         self.handle_data( b[ p ] )
223                                                         p += 1
224                                                 else :
225                                                         self.__processTag( tag )
226                                                         p = e
227                                                         if self.__waitTag :
228                                                                 e = b.find( '</' , p )
229                                                                 if e != -1 :
230                                                                         self.handle_data( b[ p : e ] )
231                                                                         p = e
232                                                                         self.__waitTag = None
233                                                                 else :
234                                                                         break
235                                         elif b.startswith( '<![CDATA[' , p ) :
236                                                 e = b.find( ']]>' , p + 9 )
237                                                 if e == -1 :
238                                                         break
239                                                 else :
240                                                         cdata = b[ p + 9 : e ]
241                                                         p = e + 3
242                                                 self.handle_data( cdata )
243                                         elif b.startswith( '<!--' , p ) :
244                                                 e , s = b.find( '-->' , p + 4 ) , 3
245                                                 if e == -1 :
246                                                         e , s = b.find( '->' , p + 4 ) , 2
247                                                 if e == -1 :
248                                                         e , s = b.find( '>' , p + 4 ) , 1
249                                                 if e == -1 : # Unterminated comment
250                                                         break
251                                                 else :
252                                                         comment = b[ p + 4 : e ]
253                                                         p = e + s
254                                                 self.handle_comment( comment )
255                                         else : # b.startswith( '<!' )
256                                                 # Discard it.
257                                                 e = b.find( '>' , p + 2 ) # We only handle "simple" declaration.
258                                                 if e == -1 :
259                                                         break
260                                                 else :
261                                                         self.handle_decl( b[ p + 2 : e ] )
262                                                         p = e + 1
263                                 elif b[ p ] == '&' :
264                                         r = reEntity.match( b , p )
265                                         if r is None :
266                                                 if len( b ) - p > 3 :
267                                                         self.handle_data( '&' )
268                                                         p += 1
269                                                 else :
270                                                         break
271                                         else :
272                                                 if r.group( 1 ) is not None :
273                                                         ref = r.group( 1 )
274                                                         if not finalize and not ref.endswith( ';' ) and r.end( 0 ) == len( b ) :
275                                                                 break
276                                                         self.handle_charref( ref )
277                                                 else :
278                                                         ref = r.group( 2 )
279                                                         if not finalize and not ref.endswith( ';' ) and r.end( 0 ) == len( b ) :
280                                                                 break
281                                                         self.handle_entityref( ref )
282                                                 p = r.end( 0 )
283                                 else :
284                                         r = reEndOfData.search( b , p )
285                                         if r is None :
286                                                 if not finalize :
287                                                         break # wait for end of data
288                                                 data = b[ p : ]
289                                                 p = len( b )
290                                         else :
291                                                 e = r.start( 0 )
292                                                 data = b[ p : e ]
293                                                 p = e
294                                         self.handle_data( data )
295                 if finalize :
296                         if p < len( b ) :
297                                 self.handle_data( b[ p : ] )
298                                 p = len( b )
299                 self.__buffer , self.__pos = b , p
300
301         def __processTag( self , tag ) :
302
303                 if tag.startswith( '<!' ) :
304                         self.handle_decl( tag[ 2 : -1 ] )
305                 else :
306                         tagContents = tag[ 1 : -1 ]
307                         tagType = 0 # 0: start, 1: end, 2: empty
308                         if tagContents.startswith( '/' ) :
309                                 tagType = 1
310                                 tagContents = tagContents[ 1 : ]
311                         elif tagContents.endswith( '/' ) : # and ' ' not in tagContents :
312                                 tagType = 2
313                                 tagContents = tagContents[ : -1 ]
314                         r = reTagName.match( tagContents )
315                         if r is not None :
316                                 e = r.end( 0 )
317                                 name , attr = tagContents[ : e ] , tagContents[ e : ]
318                                 attr = _parseAttr( attr )
319                                 if tagType == 0 :
320                                         self.handle_starttag( name , attr )
321                                         name = name.lower()
322                                         if name in self.__cdataTags :
323                                                 self.__waitTag = name # Start of CDATA element
324                                 elif tagType == 1 :
325                                         self.handle_endtag( name )
326                                 elif tagType == 2 :
327                                         self.handle_startendtag( name , attr )
328                                 else :
329                                         raise HTMLParser
330                         else :
331                                 self.handle_data( tag )
332
333 class HTMLParserDebug( HTMLParser ) :
334
335         def handle_data( self , data ) :
336
337                 print 'data(%r)' % data
338
339         def handle_starttag( self , name , attr ) :
340
341                 print 'starttag(%r,%r)' % ( name , attr )
342
343         def handle_endtag( self , name ) :
344
345                 print 'endtag(%r)' % name
346
347         def handle_startendtag( self , name , attr ) :
348
349                 print 'startendtag(%r,%r)...' % ( name , attr )
350                 HTMLParser.handle_startendtag( self , name , attr )
351
352         def handle_charref( self , name ) :
353
354                 print 'charref(%r)' % name
355
356         def handle_entityref( self , name ) :
357
358                 print 'entityref(%r)' % name
359
360         def handle_comment( self , data ) :
361
362                 print 'comment(%r)' % data
363
364         def handle_decl( self , data ) :
365
366                 print 'decl(%r)' % data
367
368         def handle_pi( self , data ) :
369
370                 print 'pi(%r)' % data
371
372 # Local Variables:
373 # tab-width: 4
374 # python-indent: 4
375 # End: