summaryrefslogtreecommitdiffstats
path: root/Lib/sgmllib.py
blob: 17e5745f0ba4115a06f22a5357df81d2e0ae7259 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
# A parser for SGML, using the derived class as static DTD.

# XXX This only supports those SGML features used by HTML.

# XXX There should be a way to distinguish between PCDATA (parsed
# character data -- the normal case), RCDATA (replaceable character
# data -- only char and entity references and end tags are special)
# and CDATA (character data -- only end tags are special).


import regex
import string


# Regular expressions used for parsing

incomplete = regex.compile(
	  '<!-?\|</[a-zA-Z][a-zA-Z0-9]*[ \t\n]*\|</?\|' +
	  '&#[a-zA-Z0-9]*\|&[a-zA-Z][a-zA-Z0-9]*\|&')
entityref = regex.compile('&[a-zA-Z][a-zA-Z0-9]*[;.]')
charref = regex.compile('&#[a-zA-Z0-9]+;')
starttagopen = regex.compile('<[a-zA-Z]')
endtag = regex.compile('</[a-zA-Z][a-zA-Z0-9]*[ \t\n]*>')
commentopen = regex.compile('<!--')


# SGML parser base class -- find tags and call handler functions.
# Usage: p = SGMLParser(); p.feed(data); ...; p.close().
# The dtd is defined by deriving a class which defines methods
# with special names to handle tags: start_foo and end_foo to handle
# <foo> and </foo>, respectively, or do_foo to handle <foo> by itself.
# (Tags are converted to lower case for this purpose.)  The data
# between tags is passed to the parser by calling self.handle_data()
# with some data as argument (the data may be split up in arbutrary
# chunks).  Entity references are passed by calling
# self.handle_entityref() with the entity reference as argument.

class SGMLParser:

	# Interface -- initialize and reset this instance
	def __init__(self):
		self.reset()

	# Interface -- reset this instance.  Loses all unprocessed data
	def reset(self):
		self.rawdata = ''
		self.stack = []
		self.nomoretags = 0
		self.literal = 0

	# For derived classes only -- enter literal mode (CDATA) till EOF
	def setnomoretags(self):
		self.nomoretags = self.literal = 1

	# For derived classes only -- enter literal mode (CDATA)
	def setliteral(self, *args):
		self.literal = 1

	# Interface -- feed some data to the parser.  Call this as
	# often as you want, with as little or as much text as you
	# want (may include '\n').  (This just saves the text, all the
	# processing is done by goahead().)
	def feed(self, data):
		self.rawdata = self.rawdata + data
		self.goahead(0)

	# Interface -- handle the remaining data
	def close(self):
		self.goahead(1)

	# Internal -- handle data as far as reasonable.  May leave state
	# and data to be processed by a subsequent call.  If 'end' is
	# true, force handling all data as if followed by EOF marker.
	def goahead(self, end):
		rawdata = self.rawdata
		i = 0
		n = len(rawdata)
		while i < n:
			if self.nomoretags:
				self.handle_data(rawdata[i:n])
				i = n
				break
			j = incomplete.search(rawdata, i)
			if j < 0: j = n
			if i < j: self.handle_data(rawdata[i:j])
			i = j
			if i == n: break
			if rawdata[i] == '<':
				if starttagopen.match(rawdata, i) >= 0:
					if self.literal:
						self.handle_data(rawdata[i])
						i = i+1
						continue
					k = self.parse_starttag(i)
					if k < 0: break
					i = i + k
					continue
				k = endtag.match(rawdata, i)
				if k >= 0:
					j = i+k
					self.parse_endtag(rawdata[i:j])
					i = j
					self.literal = 0
					continue
				if commentopen.match(rawdata, i) >= 0:
					if self.literal:
						self.handle_data(rawdata[i])
						i = i+1
						continue
					k = self.parse_comment(i)
					if k < 0: break
					i = i+k
					continue
			elif rawdata[i] == '&':
				k = charref.match(rawdata, i)
				if k >= 0:
					j = i+k
					self.handle_charref(rawdata[i+2:j-1])
					i = j
					continue
				k = entityref.match(rawdata, i)
				if k >= 0:
					j = i+k
					self.handle_entityref(rawdata[i+1:j-1])
					i = j
					continue
			else:
				raise RuntimeError, 'neither < nor & ??'
			# We get here only if incomplete matches but
			# nothing else
			k = incomplete.match(rawdata, i)
			if k < 0: raise RuntimeError, 'no incomplete match ??'
			j = i+k
			if j == n: break # Really incomplete
			self.handle_data(rawdata[i:j])
			i = j
		# end while
		if end and i < n:
			self.handle_data(rawdata[i:n])
			i = n
		self.rawdata = rawdata[i:]
		# XXX if end: check for empty stack

	# Internal -- parse comment, return length or -1 if not ternimated
	def parse_comment(self, i):
		rawdata = self.rawdata
		if rawdata[i:i+4] <> '<!--':
			raise RuntimeError, 'unexpected call to handle_comment'
		try:
			j = string.index(rawdata, '--', i+4)
		except string.index_error:
			return -1
		self.handle_comment(rawdata[i+4: j])
		j = j+2
		n = len(rawdata)
		while j < n and rawdata[j] in ' \t\n': j = j+1
		if j == n: return -1 # Wait for final '>'
		if rawdata[j] == '>':
			j = j+1
		else:
			print '*** comment not terminated with >'
			print repr(rawdata[j-5:j]), '*!*', repr(rawdata[j:j+5])
		return j-i

	# Internal -- handle starttag, return length or -1 if not terminated
	def parse_starttag(self, i):
		rawdata = self.rawdata
		try:
			j = string.index(rawdata, '>', i)
		except string.index_error:
			return -1
		# Now parse the data between i+1 and j into a tag and attrs
		attrs = []
		tagfind = regex.compile('[a-zA-Z][a-zA-Z0-9]*')
		attrfind = regex.compile(
		  '[ \t\n]+\([a-zA-Z][a-zA-Z0-9]*\)' +
		  '\([ \t\n]*=[ \t\n]*' +
		     '\(\'[^\']*\';\|"[^"]*"\|[-a-zA-Z0-9./:+*%?!()_#]+\)\)?')
		k = tagfind.match(rawdata, i+1)
		if k < 0:
			raise RuntimeError, 'unexpected call to parse_starttag'
		k = i+1+k
		tag = string.lower(rawdata[i+1:k])
		while k < j:
			l = attrfind.match(rawdata, k)
			if l < 0: break
			regs = attrfind.regs
			a1, b1 = regs[1]
			a2, b2 = regs[2]
			a3, b3 = regs[3]
			attrname = rawdata[a1:b1]
			if '=' in rawdata[k:k+l]:
				attrvalue = rawdata[a3:b3]
				if attrvalue[:1] == '\'' == attrvalue[-1:] or \
				   attrvalue[:1] == '"' == attrvalue[-1:]:
					attrvalue = attrvalue[1:-1]
			else:
				attrvalue = ''
			attrs.append((string.lower(attrname), attrvalue))
			k = k + l
		j = j+1
		try:
			method = getattr(self, 'start_' + tag)
		except AttributeError:
			try:
				method = getattr(self, 'do_' + tag)
			except AttributeError:
				self.unknown_starttag(tag, attrs)
				return j-i
			method(attrs)
			return j-i
		self.stack.append(tag)
		method(attrs)
		return j-i

	# Internal -- parse endtag
	def parse_endtag(self, data):
		if data[:2] <> '</' or data[-1:] <> '>':
			raise RuntimeError, 'unexpected call to parse_endtag'
		tag = string.lower(string.strip(data[2:-1]))
		try:
			method = getattr(self, 'end_' + tag)
		except AttributeError:
			self.unknown_endtag(tag)
			return
		if self.stack and self.stack[-1] == tag:
			del self.stack[-1]
		else:
			print '*** Unbalanced </' + tag + '>'
			print '*** Stack:', self.stack
			found = None
			for i in range(len(self.stack)):
				if self.stack[i] == tag: found = i
			if found <> None:
				del self.stack[found:]
		method()

	# Example -- handle character reference, no need to override
	def handle_charref(self, name):
		try:
			n = string.atoi(name)
		except string.atoi_error:
			self.unknown_charref(name)
			return
		if not 0 <= n <= 255:
			self.unknown_charref(name)
			return
		self.handle_data(chr(n))

	# Definition of entities -- derived classes may override
	entitydefs = \
		{'lt': '<', 'gt': '>', 'amp': '&', 'quot': '"', 'apos': '\''}

	# Example -- handle entity reference, no need to override
	def handle_entityref(self, name):
		table = self.entitydefs
		name = string.lower(name)
		if table.has_key(name):
			self.handle_data(table[name])
		else:
			self.unknown_entityref(name)
			return

	# Example -- handle data, should be overridden
	def handle_data(self, data):
		pass

	# Example -- handle comment, could be overridden
	def handle_comment(self, data):
		pass

	# To be overridden -- handlers for unknown objects
	def unknown_starttag(self, tag, attrs): pass
	def unknown_endtag(self, tag): pass
	def unknown_charref(self, ref): pass
	def unknown_entityref(self, ref): pass


class TestSGML(SGMLParser):

	def handle_data(self, data):
		r = repr(data)
		if len(r) > 72:
			r = r[:35] + '...' + r[-35:]
		print 'data:', r

	def handle_comment(self, data):
		r = repr(data)
		if len(r) > 68:
			r = r[:32] + '...' + r[-32:]
		print 'comment:', r

	def unknown_starttag(self, tag, attrs):
		print 'start tag: <' + tag,
		for name, value in attrs:
			print name + '=' + '"' + value + '"',
		print '>'

	def unknown_endtag(self, tag):
		print 'end tag: </' + tag + '>'

	def unknown_entityref(self, ref):
		print '*** unknown entity ref: &' + ref + ';'

	def unknown_charref(self, ref):
		print '*** unknown char ref: &#' + ref + ';'


def test():
	file = 'test.html'
	f = open(file, 'r')
	x = TestSGML()
	while 1:
		line = f.readline()
		if not line:
			x.close()
			break
		x.feed(line)


if __name__ == '__main__':
	test()