Fixed / overloading when enabling true division.
[tx] / htmltree.py
1 # -*- coding: utf-8 -*-
2
3 # htmltree.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 __all__ = [
21         'Parser' ,
22         'parse' ,
23         'parseFile'
24         ]
25
26 import re
27 from htmlentitydefs import entitydefs
28
29 from error import Error
30 from nodes import Document, Element, Attribute, Comment, Text, ProcessingInstruction
31 from htmlparser import HTMLParser, HTMLParseError
32 from misc import typeOf, decodeData
33
34 reSpace = re.compile( r'\s' )
35 reNotSpace = re.compile( r'\S' )
36
37 def normalizeSpace( s ) :
38
39         if reSpace.search( s ) is not None :
40                 return ' '.join( s.split() )
41         else :
42                 return s
43
44 class Parser( HTMLParser ) :
45
46         def __init__( self , preserveCase = False ) :
47
48                 HTMLParser.__init__( self )
49                 self.__stack = [ ( None , None , [] ) ] # List of RXP style elements
50                 self.__skipBlank = False
51                 self.__caseSensitive = preserveCase
52                 #
53                 # Empty elements.
54                 #
55                 self.emptyElements = set( ( 'param' , 'hr' , 'br' , 'input' , 'meta' , 'link' , 'area' , 'img' , 'meta' , 'frame' ) )
56                 #
57                 # Elements that shouldn't have ancestor with the same name.
58                 #
59                 self.redundantElements = set( ( 'script' , 'form' , 'p' , 'span' , 'option' ) )
60                 #
61                 # Elements that shouldn't have parent with the same name.
62                 #
63                 self.directRedundantElements = set( ( 'table' , 'tr' , 'td' , 'li' , 'ul' , 'ol' ) )
64
65         def __addText( self , data ) :
66
67                 #
68                 # Close current elements which should not contains text.
69                 #
70                 s = self.__stack
71                 while 1 :
72                         top = s[ -1 ][ 0 ]
73                         if top not in self.emptyElements :
74                                 break
75                         name , attrs , children = s.pop()
76                         s[ -1 ][ 2 ].append( Element( name , attrs , children ) )
77                 #
78                 # Add the Text node.
79                 #
80                 s[ -1 ][ 2 ].append( Text( data ) )
81
82         def __closeElementsAsNeeded( self , tag ) :
83
84                 '''Close any elements such that opening a new element with name 'tag'
85                 ensure that document is still valid accordingly to
86                 directRedundantElements and redundantElements restrictions.'''
87
88                 s = self.__stack
89                 #
90                 # Close current elements which should not have children.
91                 #
92                 while 1 :
93                         top = s[ -1 ][ 0 ]
94                         if top not in self.emptyElements :
95                                 break
96                         name , attrs , children = s.pop()
97                         s[ -1 ][ 2 ].append( Element( name , attrs , children ) )
98                 if tag in self.directRedundantElements :
99                         #
100                         # Parent with same name ?
101                         #
102                         if s[ -1 ][ 0 ] == tag :
103                                 name , attrs , children = s.pop()
104                                 s[ -1 ][ 2 ].append( Element( name , attrs , children ) )
105                 elif tag in self.redundantElements :
106                         #
107                         # Ancestor with same name ?
108                         #
109                         i = len( s )
110                         while i > 0 :
111                                 i -= 1
112                                 if s[ i ][ 0 ] == tag :
113                                         for j in range( len( s ) - i ) :
114                                                 name , attrs , children = s.pop()
115                                                 s[ -1 ][ 2 ].append( Element( name , attrs , children ) )
116                                         break
117
118         def handle_starttag( self , tag , attrs ) :
119
120                 if not self.__caseSensitive :
121                         tag = tag.lower()
122
123                 self.__closeElementsAsNeeded( tag )
124
125                 attributeNodes = {}
126                 if not self.__caseSensitive :
127                         for name , value in attrs :
128                                 name = name.lower()
129                                 if value is None :
130                                         value = name
131                                 if name not in attributeNodes :
132                                         attributeNodes[ name ] = normalizeSpace( value )
133                 else :
134                         for name , value in attrs :
135                                 if value is None :
136                                         value = name
137                                 if name not in attributeNodes :
138                                         attributeNodes[ name ] = normalizeSpace( value )
139                 attrs = [ Attribute( name , value ) for name , value in attributeNodes.items() ]
140                 self.__stack.append( ( tag , attrs , [] ) )
141
142         def handle_endtag( self , tag ) :
143
144                 if not self.__caseSensitive :
145                         tag = tag.lower()
146
147                 #
148                 # Check if tag close an already opened tag.
149                 #
150                 s = self.__stack
151                 i = len( s )
152                 while i > 0 :
153                         i -= 1
154                         if s[ i ][ 0 ] == tag :
155                                 for j in range( len( s ) - i ) :
156                                         name , attrs , children = s.pop()
157                                         s[ -1 ][ 2 ].append( Element( name , attrs , children ) )
158                                 break
159
160         def handle_data( self , data ) :
161
162                 if not self.__skipBlank or reNotSpace.search( data ) is not None :
163                         self.__addText( data )
164
165         def handle_comment( self , data ) :
166
167                 #
168                 # Script and style elements cannot contains comment (because
169                 # they are declared as CDATA) so we treat comment as regular
170                 # text.  However, the parser should have handled that
171                 # correctly, so we should not expect comment inside script and
172                 # style element at this point.
173                 #
174                 s = self.__stack
175                 if s[ -1 ][ 0 ] and s[ -1 ][ 0 ].lower() == 'script' :
176                         s[ -1 ][ 2 ].append( Text( data ) )
177                 else :
178                         comment = data.replace( '--' , '- -' ).replace( '--' , '- -' )
179                         if comment.endswith( '-' ) :
180                                 comment += ' '
181                         s[ -1 ][ 2 ].append( Comment( comment ) )
182
183         def handle_charref( self , name ) :
184
185                 try :
186                         n = int( name )
187                 except :
188                         data = '?' # FIXME: Or ignore ?
189                 else :
190                         data = unichr( n )
191                 self.__addText( data )
192
193         def handle_entityref( self , name ) :
194
195                 character = entitydefs.get( name ) 
196                 if character :
197                         data = character.decode( 'iso-8859-1' )
198                 else :
199                         # FIXME: Or ignore ?
200                         data = '&' + name + ';'
201                 self.__addText( data )
202
203         def handle_pi( self , data ) :
204
205                 data = data.split( None , 1 )
206                 if len( data ) == 1 :
207                         target , content = data[ 0 ] , ''
208                 elif len( data ) == 2 :
209                         target , content = data
210                 else :
211                         target , content = None , None
212                 if target is not None and target.lower() != 'xml' :
213                         pi = ProcessingInstruction( target , content )
214                         self.__stack[ -1 ][ 2 ].append( pi )
215
216         def close( self ) :
217
218                 try :
219                         HTMLParser.close( self )
220                 except HTMLParseError :
221                         pass
222                 s = self.__stack
223                 while len( s ) > 1 :
224                         name , attrs , children = s.pop()
225                         s[ -1 ][ 2 ].append( Element( name , attrs , children ) )
226                 return Document( s[ 0 ][ 2 ] )
227
228 def parse( data , preserveCase = False ) :
229
230         hp = Parser( preserveCase )
231         hp.feed( decodeData( data ) )
232         return hp.close()
233
234 # Local Variables:
235 # tab-width: 4
236 # python-indent: 4
237 # End: