Reformatted with 4-space tab stops.
Allow '=' and '~' in unquoted attribute values. Added overridable methods handle_starttag(tag, method, attrs) and handle_endtag(tag, method) so subclasses can decide whether they really want to call the method (e.g. when suppressing some portion of the document). Added support for a number of SGML shortcuts: shorthand full notation <tag>...<>... <tag>...<tag>... <tag>...</> <tag>...</tag> <tag/.../ <tag>...</tag> <tag1<tag2> <tag1><tag2> </tag1</tag2> </tag1></tag2> </tag1<tag2> </tag1><tag2> This required factoring out some common actions and rationalizing the interface to parse_endtag(), so as to make the code more readable. Fixed syntax for &entity and &#char references so the trailing semicolon is optional; removed explicit support for trailing period (which was a TBL mistake in HTML 0.0). Generalized the test program. Tried to speed things up a little. (More to come after the profile results are in.) Fix error recovery: call the end methods popped from the stack instead of the one that triggers. (Plus some complications because of the way HTML extensions are handled in Grail.)
This commit is contained in:
parent
5f204775bf
commit
48766512a0
248
Lib/sgmllib.py
248
Lib/sgmllib.py
|
@ -14,16 +14,28 @@ 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]*>')
|
||||
interesting = regex.compile('[&<]')
|
||||
incomplete = regex.compile('&\([a-zA-Z][a-zA-Z0-9]*\|#[0-9]*\)?\|'
|
||||
'<\([a-zA-Z][^<>]*\|'
|
||||
'/\([a-zA-Z][^<>]*\)?\|'
|
||||
'![^<>]*\)?')
|
||||
|
||||
entityref = regex.compile('&\([a-zA-Z][a-zA-Z0-9]*\)[^a-zA-Z0-9]')
|
||||
charref = regex.compile('&#\([0-9]+\)[^0-9]')
|
||||
|
||||
starttagopen = regex.compile('<[>a-zA-Z]')
|
||||
shorttagopen = regex.compile('<[a-zA-Z][a-zA-Z0-9]*/')
|
||||
shorttag = regex.compile('<\([a-zA-Z][a-zA-Z0-9]*\)/\([^/]*\)/')
|
||||
endtagopen = regex.compile('</[<>a-zA-Z]')
|
||||
endbracket = regex.compile('[<>]')
|
||||
special = regex.compile('<![^<>]*>')
|
||||
commentopen = regex.compile('<!--')
|
||||
commentclose = regex.compile('--[ \t\n]*>')
|
||||
tagfind = regex.compile('[a-zA-Z][a-zA-Z0-9]*')
|
||||
attrfind = regex.compile(
|
||||
'[ \t\n]+\([a-zA-Z_][a-zA-Z_0-9]*\)'
|
||||
'\([ \t\n]*=[ \t\n]*'
|
||||
'\(\'[^\']*\'\|"[^"]*"\|[-a-zA-Z0-9./:+*%?!()_#=~]*\)\)?')
|
||||
|
||||
|
||||
# SGML parser base class -- find tags and call handler functions.
|
||||
|
@ -48,6 +60,7 @@ class SGMLParser:
|
|||
def reset(self):
|
||||
self.rawdata = ''
|
||||
self.stack = []
|
||||
self.lasttag = '???'
|
||||
self.nomoretags = 0
|
||||
self.literal = 0
|
||||
|
||||
|
@ -83,7 +96,7 @@ class SGMLParser:
|
|||
self.handle_data(rawdata[i:n])
|
||||
i = n
|
||||
break
|
||||
j = incomplete.search(rawdata, i)
|
||||
j = interesting.search(rawdata, i)
|
||||
if j < 0: j = n
|
||||
if i < j: self.handle_data(rawdata[i:j])
|
||||
i = j
|
||||
|
@ -96,13 +109,12 @@ class SGMLParser:
|
|||
continue
|
||||
k = self.parse_starttag(i)
|
||||
if k < 0: break
|
||||
i = i + k
|
||||
i = k
|
||||
continue
|
||||
k = endtag.match(rawdata, i)
|
||||
if k >= 0:
|
||||
j = i+k
|
||||
self.parse_endtag(rawdata[i:j])
|
||||
i = j
|
||||
if endtagopen.match(rawdata, i) >= 0:
|
||||
k = self.parse_endtag(i)
|
||||
if k < 0: break
|
||||
i = k
|
||||
self.literal = 0
|
||||
continue
|
||||
if commentopen.match(rawdata, i) >= 0:
|
||||
|
@ -125,24 +137,31 @@ class SGMLParser:
|
|||
elif rawdata[i] == '&':
|
||||
k = charref.match(rawdata, i)
|
||||
if k >= 0:
|
||||
j = i+k
|
||||
self.handle_charref(rawdata[i+2:j-1])
|
||||
i = j
|
||||
k = i+k
|
||||
if rawdata[k-1] != ';': k = k-1
|
||||
name = charref.group(1)
|
||||
self.handle_charref(name)
|
||||
i = k
|
||||
continue
|
||||
k = entityref.match(rawdata, i)
|
||||
if k >= 0:
|
||||
j = i+k
|
||||
self.handle_entityref(rawdata[i+1:j-1])
|
||||
i = j
|
||||
k = i+k
|
||||
if rawdata[k-1] != ';': k = k-1
|
||||
name = entityref.group(1)
|
||||
self.handle_entityref(name)
|
||||
i = k
|
||||
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 ??'
|
||||
if k < 0:
|
||||
self.handle_data(rawdata[i])
|
||||
i = i+1
|
||||
continue
|
||||
j = i+k
|
||||
if j == n or rawdata[i:i+2] == '<!':
|
||||
if j == n:
|
||||
break # Really incomplete
|
||||
self.handle_data(rawdata[i:j])
|
||||
i = j
|
||||
|
@ -168,22 +187,38 @@ class SGMLParser:
|
|||
# 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:
|
||||
if shorttagopen.match(rawdata, i) >= 0:
|
||||
# SGML shorthand: <tag/data/ == <tag>data</tag>
|
||||
# XXX Can data contain &... (entity or char refs)?
|
||||
# XXX Can data contain < or > (tag characters)?
|
||||
# XXX Can there be whitespace before the first /?
|
||||
j = shorttag.match(rawdata, i)
|
||||
if j < 0:
|
||||
return -1
|
||||
tag, data = shorttag.group(1, 2)
|
||||
tag = string.lower(tag)
|
||||
self.finish_shorttag(tag, data)
|
||||
k = i+j
|
||||
if rawdata[k-1] == '<':
|
||||
k = k-1
|
||||
return k
|
||||
# XXX The following should skip matching quotes (' or ")
|
||||
j = endbracket.search(rawdata, i+1)
|
||||
if j < 0:
|
||||
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-Z_0-9]*\)' +
|
||||
'\([ \t\n]*=[ \t\n]*' +
|
||||
'\(\'[^\']*\'\|"[^"]*"\|[-a-zA-Z0-9./:+*%?!()_#]*\)\)?')
|
||||
if rawdata[i:i+2] == '<>':
|
||||
# SGML shorthand: <> == <last open tag seen>
|
||||
k = j
|
||||
tag = self.lasttag
|
||||
else:
|
||||
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])
|
||||
self.lasttag = tag
|
||||
while k < j:
|
||||
l = attrfind.match(rawdata, k)
|
||||
if l < 0: break
|
||||
|
@ -195,7 +230,32 @@ class SGMLParser:
|
|||
attrvalue = attrvalue[1:-1]
|
||||
attrs.append((string.lower(attrname), attrvalue))
|
||||
k = k + l
|
||||
if rawdata[j] == '>':
|
||||
j = j+1
|
||||
self.finish_starttag(tag, attrs)
|
||||
return j
|
||||
|
||||
# Internal -- parse endtag
|
||||
def parse_endtag(self, i):
|
||||
rawdata = self.rawdata
|
||||
j = endbracket.search(rawdata, i+1)
|
||||
if j < 0:
|
||||
return -1
|
||||
tag = string.lower(string.strip(rawdata[i+2:j]))
|
||||
if rawdata[j] == '>':
|
||||
j = j+1
|
||||
self.finish_endtag(tag)
|
||||
return j
|
||||
|
||||
# Internal -- finish parsing of <tag/data/ (same as <tag>data</tag>)
|
||||
def finish_shorttag(self, tag, data):
|
||||
self.finish_starttag(tag, [])
|
||||
self.handle_data(data)
|
||||
self.finish_endtag(tag)
|
||||
|
||||
# Internal -- finish processing of start tag
|
||||
# Return -1 for unknown tag, 0 for open-only tag, 1 for balanced tag
|
||||
def finish_starttag(self, tag, attrs):
|
||||
try:
|
||||
method = getattr(self, 'start_' + tag)
|
||||
except AttributeError:
|
||||
|
@ -203,35 +263,50 @@ class SGMLParser:
|
|||
method = getattr(self, 'do_' + tag)
|
||||
except AttributeError:
|
||||
self.unknown_starttag(tag, attrs)
|
||||
return j-i
|
||||
method(attrs)
|
||||
return j-i
|
||||
return -1
|
||||
else:
|
||||
self.handle_starttag(tag, method, attrs)
|
||||
return 0
|
||||
else:
|
||||
self.stack.append(tag)
|
||||
method(attrs)
|
||||
return j-i
|
||||
self.handle_starttag(tag, method, attrs)
|
||||
return 1
|
||||
|
||||
# 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]))
|
||||
# Internal -- finish processing of end tag
|
||||
def finish_endtag(self, tag):
|
||||
if not tag:
|
||||
found = len(self.stack) - 1
|
||||
if found < 0:
|
||||
self.unknown_endtag(tag)
|
||||
return
|
||||
else:
|
||||
if tag not in self.stack:
|
||||
try:
|
||||
method = getattr(self, 'end_' + tag)
|
||||
except AttributeError:
|
||||
self.unknown_endtag(tag)
|
||||
return
|
||||
# XXX Should invoke end methods when popping their
|
||||
# XXX stack entry, not when encountering the tag!
|
||||
if self.stack and self.stack[-1] == tag:
|
||||
del self.stack[-1]
|
||||
else:
|
||||
self.report_unbalanced(tag)
|
||||
# Now repair it
|
||||
found = None
|
||||
for i in range(len(self.stack)):
|
||||
found = len(self.stack)
|
||||
for i in range(found):
|
||||
if self.stack[i] == tag: found = i
|
||||
if found <> None:
|
||||
del self.stack[found:]
|
||||
while len(self.stack) > found:
|
||||
tag = self.stack[-1]
|
||||
try:
|
||||
method = getattr(self, 'end_' + tag)
|
||||
except AttributeError:
|
||||
method = None
|
||||
if method:
|
||||
self.handle_endtag(tag, method)
|
||||
else:
|
||||
self.unknown_endtag(tag)
|
||||
del self.stack[-1]
|
||||
|
||||
# Overridable -- handle start tag
|
||||
def handle_starttag(self, tag, method, attrs):
|
||||
method(attrs)
|
||||
|
||||
# Overridable -- handle end tag
|
||||
def handle_endtag(self, tag, method):
|
||||
method()
|
||||
|
||||
# Example -- report an unbalanced </...> tag.
|
||||
|
@ -280,46 +355,91 @@ class SGMLParser:
|
|||
def unknown_entityref(self, ref): pass
|
||||
|
||||
|
||||
class TestSGML(SGMLParser):
|
||||
class TestSGMLParser(SGMLParser):
|
||||
|
||||
def __init__(self, verbose=0):
|
||||
self.testdata = ""
|
||||
SGMLParser.__init__(self, verbose)
|
||||
|
||||
def handle_data(self, data):
|
||||
r = repr(data)
|
||||
if len(r) > 72:
|
||||
r = r[:35] + '...' + r[-35:]
|
||||
print 'data:', r
|
||||
self.testdata = self.testdata + data
|
||||
if len(`self.testdata`) >= 70:
|
||||
self.flush()
|
||||
|
||||
def flush(self):
|
||||
data = self.testdata
|
||||
if data:
|
||||
self.testdata = ""
|
||||
print 'data:', `data`
|
||||
|
||||
def handle_comment(self, data):
|
||||
r = repr(data)
|
||||
self.flush()
|
||||
r = `data`
|
||||
if len(r) > 68:
|
||||
r = r[:32] + '...' + r[-32:]
|
||||
print 'comment:', r
|
||||
|
||||
def unknown_starttag(self, tag, attrs):
|
||||
self.flush()
|
||||
if not attrs:
|
||||
print 'start tag: <' + tag + '>'
|
||||
else:
|
||||
print 'start tag: <' + tag,
|
||||
for name, value in attrs:
|
||||
print name + '=' + '"' + value + '"',
|
||||
print '>'
|
||||
|
||||
def unknown_endtag(self, tag):
|
||||
self.flush()
|
||||
print 'end tag: </' + tag + '>'
|
||||
|
||||
def unknown_entityref(self, ref):
|
||||
self.flush()
|
||||
print '*** unknown entity ref: &' + ref + ';'
|
||||
|
||||
def unknown_charref(self, ref):
|
||||
self.flush()
|
||||
print '*** unknown char ref: &#' + ref + ';'
|
||||
|
||||
def close(self):
|
||||
SGMLParser.close(self)
|
||||
self.flush()
|
||||
|
||||
def test():
|
||||
|
||||
def test(args = None):
|
||||
import sys
|
||||
|
||||
if not args:
|
||||
args = sys.argv[1:]
|
||||
|
||||
if args and args[0] == '-s':
|
||||
args = args[1:]
|
||||
klass = SGMLParser
|
||||
else:
|
||||
klass = TestSGMLParser
|
||||
|
||||
if args:
|
||||
file = args[0]
|
||||
else:
|
||||
file = 'test.html'
|
||||
|
||||
if file == '-':
|
||||
f = sys.stdin
|
||||
else:
|
||||
try:
|
||||
f = open(file, 'r')
|
||||
x = TestSGML()
|
||||
while 1:
|
||||
line = f.readline()
|
||||
if not line:
|
||||
except IOError, msg:
|
||||
print file, ":", msg
|
||||
sys.exit(1)
|
||||
|
||||
data = f.read()
|
||||
if f is not sys.stdin:
|
||||
f.close()
|
||||
|
||||
x = klass()
|
||||
for c in data:
|
||||
x.feed(c)
|
||||
x.close()
|
||||
break
|
||||
x.feed(line)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
Loading…
Reference in New Issue