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
12from warnings import warnpy3k
13warnpy3k("the sgmllib module has been removed in Python 3.0",
14         stacklevel=2)
15del warnpy3k
16
17import markupbase
18import re
19
20__all__ = ["SGMLParser", "SGMLParseError"]
21
22# Regular expressions used for parsing
23
24interesting = re.compile('[&<]')
25incomplete = re.compile('&([a-zA-Z][a-zA-Z0-9]*|#[0-9]*)?|'
26                           '<([a-zA-Z][^<>]*|'
27                              '/([a-zA-Z][^<>]*)?|'
28                              '![^<>]*)?')
29
30entityref = re.compile('&([a-zA-Z][-.a-zA-Z0-9]*)[^a-zA-Z0-9]')
31charref = re.compile('&#([0-9]+)[^0-9]')
32
33starttagopen = re.compile('<[>a-zA-Z]')
34shorttagopen = re.compile('<[a-zA-Z][-.a-zA-Z0-9]*/')
35shorttag = re.compile('<([a-zA-Z][-.a-zA-Z0-9]*)/([^/]*)/')
36piclose = re.compile('>')
37endbracket = re.compile('[<>]')
38tagfind = re.compile('[a-zA-Z][-_.a-zA-Z0-9]*')
39attrfind = re.compile(
40    r'\s*([a-zA-Z_][-:.a-zA-Z_0-9]*)(\s*=\s*'
41    r'(\'[^\']*\'|"[^"]*"|[][\-a-zA-Z0-9./,:;+*%?!&$\(\)_#=~\'"@]*))?')
42
43
44class 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
60class 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
461class 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
516def 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
552if __name__ == '__main__':
553    test()
554