1 """A parser for SGML, using the derived class as a static DTD.""" 2 3 # XXX This only supports those SGML features used by HTML. 4 5 # XXX There should be a way to distinguish between PCDATA (parsed 6 # character data -- the normal case), RCDATA (replaceable character 7 # data -- only char and entity references and end tags are special) 8 # and CDATA (character data -- only end tags are special). RCDATA is 9 # not supported at all. 10 11 import _markupbase 12 import re 13 14 __all__ = ["SGMLParser", "SGMLParseError"] 15 16 # Regular expressions used for parsing 17 18 interesting = re.compile('[&<]') 19 incomplete = re.compile('&([a-zA-Z][a-zA-Z0-9]*|#[0-9]*)?|' 20 '<([a-zA-Z][^<>]*|' 21 '/([a-zA-Z][^<>]*)?|' 22 '![^<>]*)?') 23 24 entityref = re.compile('&([a-zA-Z][-.a-zA-Z0-9]*)[^a-zA-Z0-9]') 25 charref = re.compile('&#([0-9]+)[^0-9]') 26 27 starttagopen = re.compile('<[>a-zA-Z]') 28 shorttagopen = re.compile('<[a-zA-Z][-.a-zA-Z0-9]*/') 29 shorttag = re.compile('<([a-zA-Z][-.a-zA-Z0-9]*)/([^/]*)/') 30 piclose = re.compile('>') 31 endbracket = re.compile('[<>]') 32 tagfind = re.compile('[a-zA-Z][-_.a-zA-Z0-9]*') 33 attrfind = re.compile( 34 r'\s*([a-zA-Z_][-:.a-zA-Z_0-9]*)(\s*=\s*' 35 r'(\'[^\']*\'|"[^"]*"|[][\-a-zA-Z0-9./,:;+*%?!&$\(\)_#=~\'"@]*))?') 36 37 38 class SGMLParseError(RuntimeError): 39 """Exception raised for all parse errors.""" 40 pass 41 42 43 # SGML parser base class -- find tags and call handler functions. 44 # Usage: p = SGMLParser(); p.feed(data); ...; p.close(). 45 # The dtd is defined by deriving a class which defines methods 46 # with special names to handle tags: start_foo and end_foo to handle 47 # <foo> and </foo>, respectively, or do_foo to handle <foo> by itself. 48 # (Tags are converted to lower case for this purpose.) The data 49 # between tags is passed to the parser by calling self.handle_data() 50 # with some data as argument (the data may be split up in arbitrary 51 # chunks). Entity references are passed by calling 52 # self.handle_entityref() with the entity reference as argument. 53 54 class SGMLParser(_markupbase.ParserBase): 55 # Definition of entities -- derived classes may override 56 entity_or_charref = re.compile('&(?:' 57 '([a-zA-Z][-.a-zA-Z0-9]*)|#([0-9]+)' 58 ')(;?)') 59 60 def __init__(self, verbose=0): 61 """Initialize and reset this instance.""" 62 self.verbose = verbose 63 self.reset() 64 65 def reset(self): 66 """Reset this instance. Loses all unprocessed data.""" 67 self.__starttag_text = None 68 self.rawdata = '' 69 self.stack = [] 70 self.lasttag = '???' 71 self.nomoretags = 0 72 self.literal = 0 73 _markupbase.ParserBase.reset(self) 74 75 def setnomoretags(self): 76 """Enter literal mode (CDATA) till EOF. 77 78 Intended for derived classes only. 79 """ 80 self.nomoretags = self.literal = 1 81 82 def setliteral(self, *args): 83 """Enter literal mode (CDATA). 84 85 Intended for derived classes only. 86 """ 87 self.literal = 1 88 89 def feed(self, data): 90 """Feed some data to the parser. 91 92 Call this as often as you want, with as little or as much text 93 as you want (may include '\n'). (This just saves the text, 94 all the processing is done by goahead().) 95 """ 96 97 self.rawdata = self.rawdata + data 98 self.goahead(0) 99 100 def close(self): 101 """Handle the remaining data.""" 102 self.goahead(1) 103 104 def error(self, message): 105 raise SGMLParseError(message) 106 107 # Internal -- handle data as far as reasonable. May leave state 108 # and data to be processed by a subsequent call. If 'end' is 109 # true, force handling all data as if followed by EOF marker. 110 def goahead(self, end): 111 rawdata = self.rawdata 112 i = 0 113 n = len(rawdata) 114 while i < n: 115 if self.nomoretags: 116 self.handle_data(rawdata[i:n]) 117 i = n 118 break 119 match = interesting.search(rawdata, i) 120 if match: j = match.start() 121 else: j = n 122 if i < j: 123 self.handle_data(rawdata[i:j]) 124 i = j 125 if i == n: break 126 if rawdata[i] == '<': 127 if starttagopen.match(rawdata, i): 128 if self.literal: 129 self.handle_data(rawdata[i]) 130 i = i+1 131 continue 132 k = self.parse_starttag(i) 133 if k < 0: break 134 i = k 135 continue 136 if rawdata.startswith("</", i): 137 k = self.parse_endtag(i) 138 if k < 0: break 139 i = k 140 self.literal = 0 141 continue 142 if self.literal: 143 if n > (i + 1): 144 self.handle_data("<") 145 i = i+1 146 else: 147 # incomplete 148 break 149 continue 150 if rawdata.startswith("<!--", i): 151 # Strictly speaking, a comment is --.*-- 152 # within a declaration tag <!...>. 153 # This should be removed, 154 # and comments handled only in parse_declaration. 155 k = self.parse_comment(i) 156 if k < 0: break 157 i = k 158 continue 159 if rawdata.startswith("<?", i): 160 k = self.parse_pi(i) 161 if k < 0: break 162 i = i+k 163 continue 164 if rawdata.startswith("<!", i): 165 # This is some sort of declaration; in "HTML as 166 # deployed," this should only be the document type 167 # declaration ("<!DOCTYPE html...>"). 168 k = self.parse_declaration(i) 169 if k < 0: break 170 i = k 171 continue 172 elif rawdata[i] == '&': 173 if self.literal: 174 self.handle_data(rawdata[i]) 175 i = i+1 176 continue 177 match = charref.match(rawdata, i) 178 if match: 179 name = match.group(1) 180 self.handle_charref(name) 181 i = match.end(0) 182 if rawdata[i-1] != ';': i = i-1 183 continue 184 match = entityref.match(rawdata, i) 185 if match: 186 name = match.group(1) 187 self.handle_entityref(name) 188 i = match.end(0) 189 if rawdata[i-1] != ';': i = i-1 190 continue 191 else: 192 self.error('neither < nor & ??') 193 # We get here only if incomplete matches but 194 # nothing else 195 match = incomplete.match(rawdata, i) 196 if not match: 197 self.handle_data(rawdata[i]) 198 i = i+1 199 continue 200 j = match.end(0) 201 if j == n: 202 break # Really incomplete 203 self.handle_data(rawdata[i:j]) 204 i = j 205 # end while 206 if end and i < n: 207 self.handle_data(rawdata[i:n]) 208 i = n 209 self.rawdata = rawdata[i:] 210 # XXX if end: check for empty stack 211 212 # Extensions for the DOCTYPE scanner: 213 _decl_otherchars = '=' 214 215 # Internal -- parse processing instr, return length or -1 if not terminated 216 def parse_pi(self, i): 217 rawdata = self.rawdata 218 if rawdata[i:i+2] != '<?': 219 self.error('unexpected call to parse_pi()') 220 match = piclose.search(rawdata, i+2) 221 if not match: 222 return -1 223 j = match.start(0) 224 self.handle_pi(rawdata[i+2: j]) 225 j = match.end(0) 226 return j-i 227 228 def get_starttag_text(self): 229 return self.__starttag_text 230 231 # Internal -- handle starttag, return length or -1 if not terminated 232 def parse_starttag(self, i): 233 self.__starttag_text = None 234 start_pos = i 235 rawdata = self.rawdata 236 if shorttagopen.match(rawdata, i): 237 # SGML shorthand: <tag/data/ == <tag>data</tag> 238 # XXX Can data contain &... (entity or char refs)? 239 # XXX Can data contain < or > (tag characters)? 240 # XXX Can there be whitespace before the first /? 241 match = shorttag.match(rawdata, i) 242 if not match: 243 return -1 244 tag, data = match.group(1, 2) 245 self.__starttag_text = '<%s/' % tag 246 tag = tag.lower() 247 k = match.end(0) 248 self.finish_shorttag(tag, data) 249 self.__starttag_text = rawdata[start_pos:match.end(1) + 1] 250 return k 251 # XXX The following should skip matching quotes (' or ") 252 # As a shortcut way to exit, this isn't so bad, but shouldn't 253 # be used to locate the actual end of the start tag since the 254 # < or > characters may be embedded in an attribute value. 255 match = endbracket.search(rawdata, i+1) 256 if not match: 257 return -1 258 j = match.start(0) 259 # Now parse the data between i+1 and j into a tag and attrs 260 attrs = [] 261 if rawdata[i:i+2] == '<>': 262 # SGML shorthand: <> == <last open tag seen> 263 k = j 264 tag = self.lasttag 265 else: 266 match = tagfind.match(rawdata, i+1) 267 if not match: 268 self.error('unexpected call to parse_starttag') 269 k = match.end(0) 270 tag = rawdata[i+1:k].lower() 271 self.lasttag = tag 272 while k < j: 273 match = attrfind.match(rawdata, k) 274 if not match: break 275 attrname, rest, attrvalue = match.group(1, 2, 3) 276 if not rest: 277 attrvalue = attrname 278 else: 279 if (attrvalue[:1] == "'" == attrvalue[-1:] or 280 attrvalue[:1] == '"' == attrvalue[-1:]): 281 # strip quotes 282 attrvalue = attrvalue[1:-1] 283 attrvalue = self.entity_or_charref.sub( 284 self._convert_ref, attrvalue) 285 attrs.append((attrname.lower(), attrvalue)) 286 k = match.end(0) 287 if rawdata[j] == '>': 288 j = j+1 289 self.__starttag_text = rawdata[start_pos:j] 290 self.finish_starttag(tag, attrs) 291 return j 292 293 # Internal -- convert entity or character reference 294 def _convert_ref(self, match): 295 if match.group(2): 296 return self.convert_charref(match.group(2)) or \ 297 '&#%s%s' % match.groups()[1:] 298 elif match.group(3): 299 return self.convert_entityref(match.group(1)) or \ 300 '&%s;' % match.group(1) 301 else: 302 return '&%s' % match.group(1) 303 304 # Internal -- parse endtag 305 def parse_endtag(self, i): 306 rawdata = self.rawdata 307 match = endbracket.search(rawdata, i+1) 308 if not match: 309 return -1 310 j = match.start(0) 311 tag = rawdata[i+2:j].strip().lower() 312 if rawdata[j] == '>': 313 j = j+1 314 self.finish_endtag(tag) 315 return j 316 317 # Internal -- finish parsing of <tag/data/ (same as <tag>data</tag>) 318 def finish_shorttag(self, tag, data): 319 self.finish_starttag(tag, []) 320 self.handle_data(data) 321 self.finish_endtag(tag) 322 323 # Internal -- finish processing of start tag 324 # Return -1 for unknown tag, 0 for open-only tag, 1 for balanced tag 325 def finish_starttag(self, tag, attrs): 326 try: 327 method = getattr(self, 'start_' + tag) 328 except AttributeError: 329 try: 330 method = getattr(self, 'do_' + tag) 331 except AttributeError: 332 self.unknown_starttag(tag, attrs) 333 return -1 334 else: 335 self.handle_starttag(tag, method, attrs) 336 return 0 337 else: 338 self.stack.append(tag) 339 self.handle_starttag(tag, method, attrs) 340 return 1 341 342 # Internal -- finish processing of end tag 343 def finish_endtag(self, tag): 344 if not tag: 345 found = len(self.stack) - 1 346 if found < 0: 347 self.unknown_endtag(tag) 348 return 349 else: 350 if tag not in self.stack: 351 try: 352 method = getattr(self, 'end_' + tag) 353 except AttributeError: 354 self.unknown_endtag(tag) 355 else: 356 self.report_unbalanced(tag) 357 return 358 found = len(self.stack) 359 for i in range(found): 360 if self.stack[i] == tag: found = i 361 while len(self.stack) > found: 362 tag = self.stack[-1] 363 try: 364 method = getattr(self, 'end_' + tag) 365 except AttributeError: 366 method = None 367 if method: 368 self.handle_endtag(tag, method) 369 else: 370 self.unknown_endtag(tag) 371 del self.stack[-1] 372 373 # Overridable -- handle start tag 374 def handle_starttag(self, tag, method, attrs): 375 method(attrs) 376 377 # Overridable -- handle end tag 378 def handle_endtag(self, tag, method): 379 method() 380 381 # Example -- report an unbalanced </...> tag. 382 def report_unbalanced(self, tag): 383 if self.verbose: 384 print('*** Unbalanced </' + tag + '>') 385 print('*** Stack:', self.stack) 386 387 def convert_charref(self, name): 388 """Convert character reference, may be overridden.""" 389 try: 390 n = int(name) 391 except ValueError: 392 return 393 if not 0 <= n <= 127: 394 return 395 return self.convert_codepoint(n) 396 397 def convert_codepoint(self, codepoint): 398 return chr(codepoint) 399 400 def handle_charref(self, name): 401 """Handle character reference, no need to override.""" 402 replacement = self.convert_charref(name) 403 if replacement is None: 404 self.unknown_charref(name) 405 else: 406 self.handle_data(replacement) 407 408 # Definition of entities -- derived classes may override 409 entitydefs = \ 410 {'lt': '<', 'gt': '>', 'amp': '&', 'quot': '"', 'apos': '\''} 411 412 def convert_entityref(self, name): 413 """Convert entity references. 414 415 As an alternative to overriding this method; one can tailor the 416 results by setting up the self.entitydefs mapping appropriately. 417 """ 418 table = self.entitydefs 419 if name in table: 420 return table[name] 421 else: 422 return 423 424 def handle_entityref(self, name): 425 """Handle entity references, no need to override.""" 426 replacement = self.convert_entityref(name) 427 if replacement is None: 428 self.unknown_entityref(name) 429 else: 430 self.handle_data(replacement) 431 432 # Example -- handle data, should be overridden 433 def handle_data(self, data): 434 pass 435 436 # Example -- handle comment, could be overridden 437 def handle_comment(self, data): 438 pass 439 440 # Example -- handle declaration, could be overridden 441 def handle_decl(self, decl): 442 pass 443 444 # Example -- handle processing instruction, could be overridden 445 def handle_pi(self, data): 446 pass 447 448 # To be overridden -- handlers for unknown objects 449 def unknown_starttag(self, tag, attrs): pass 450 def unknown_endtag(self, tag): pass 451 def unknown_charref(self, ref): pass 452 def unknown_entityref(self, ref): pass 453 454 455 class TestSGMLParser(SGMLParser): 456 457 def __init__(self, verbose=0): 458 self.testdata = "" 459 SGMLParser.__init__(self, verbose) 460 461 def handle_data(self, data): 462 self.testdata = self.testdata + data 463 if len(repr(self.testdata)) >= 70: 464 self.flush() 465 466 def flush(self): 467 data = self.testdata 468 if data: 469 self.testdata = "" 470 print('data:', repr(data)) 471 472 def handle_comment(self, data): 473 self.flush() 474 r = repr(data) 475 if len(r) > 68: 476 r = r[:32] + '...' + r[-32:] 477 print('comment:', r) 478 479 def unknown_starttag(self, tag, attrs): 480 self.flush() 481 if not attrs: 482 print('start tag: <' + tag + '>') 483 else: 484 print('start tag: <' + tag, end=' ') 485 for name, value in attrs: 486 print(name + '=' + '"' + value + '"', end=' ') 487 print('>') 488 489 def unknown_endtag(self, tag): 490 self.flush() 491 print('end tag: </' + tag + '>') 492 493 def unknown_entityref(self, ref): 494 self.flush() 495 print('*** unknown entity ref: &' + ref + ';') 496 497 def unknown_charref(self, ref): 498 self.flush() 499 print('*** unknown char ref: &#' + ref + ';') 500 501 def unknown_decl(self, data): 502 self.flush() 503 print('*** unknown decl: [' + data + ']') 504 505 def close(self): 506 SGMLParser.close(self) 507 self.flush() 508 509 510 def test(args = None): 511 import sys 512 513 if args is None: 514 args = sys.argv[1:] 515 516 if args and args[0] == '-s': 517 args = args[1:] 518 klass = SGMLParser 519 else: 520 klass = TestSGMLParser 521 522 if args: 523 file = args[0] 524 else: 525 file = 'test.html' 526 527 if file == '-': 528 f = sys.stdin 529 else: 530 try: 531 f = open(file, 'r') 532 except IOError as msg: 533 print(file, ":", msg) 534 sys.exit(1) 535 536 data = f.read() 537 if f is not sys.stdin: 538 f.close() 539 540 x = klass() 541 for c in data: 542 x.feed(c) 543 x.close() 544 545 546 if __name__ == '__main__': 547 test() 548