~inkscape.dev/inkscape-devlibs64/trunk

« back to all changes in this revision

Viewing changes to python/Lib/sgmllib.py

  • Committer: Eduard Braun
  • Date: 2016-10-22 16:51:19 UTC
  • Revision ID: eduard.braun2@gmx.de-20161022165119-9eosgy6lp8j1kzli
Update Python to version 2.7.12

Included modules:
  coverage 4.2
  lxml 3.6.4
  numpy 1.11.2
  scour 0.35
  six 1.10.0

Show diffs side-by-side

added added

removed removed

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