Index: third_party/google-endpoints/future/backports/html/parser.py |
diff --git a/third_party/google-endpoints/future/backports/html/parser.py b/third_party/google-endpoints/future/backports/html/parser.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..7b8cdba6138ee0786637940087ad8adf38008937 |
--- /dev/null |
+++ b/third_party/google-endpoints/future/backports/html/parser.py |
@@ -0,0 +1,537 @@ |
+"""A parser for HTML and XHTML. |
+ |
+Backported for python-future from Python 3.3. |
+""" |
+ |
+# This file is based on sgmllib.py, but the API is slightly different. |
+ |
+# 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). |
+ |
+from __future__ import (absolute_import, division, |
+ print_function, unicode_literals) |
+from future.builtins import * |
+from future.backports import _markupbase |
+import re |
+import warnings |
+ |
+# Regular expressions used for parsing |
+ |
+interesting_normal = re.compile('[&<]') |
+incomplete = re.compile('&[a-zA-Z#]') |
+ |
+entityref = re.compile('&([a-zA-Z][-.a-zA-Z0-9]*)[^a-zA-Z0-9]') |
+charref = re.compile('&#(?:[0-9]+|[xX][0-9a-fA-F]+)[^0-9a-fA-F]') |
+ |
+starttagopen = re.compile('<[a-zA-Z]') |
+piclose = re.compile('>') |
+commentclose = re.compile(r'--\s*>') |
+tagfind = re.compile('([a-zA-Z][-.a-zA-Z0-9:_]*)(?:\s|/(?!>))*') |
+# see http://www.w3.org/TR/html5/tokenization.html#tag-open-state |
+# and http://www.w3.org/TR/html5/tokenization.html#tag-name-state |
+tagfind_tolerant = re.compile('[a-zA-Z][^\t\n\r\f />\x00]*') |
+# Note: |
+# 1) the strict attrfind isn't really strict, but we can't make it |
+# correctly strict without breaking backward compatibility; |
+# 2) if you change attrfind remember to update locatestarttagend too; |
+# 3) if you change attrfind and/or locatestarttagend the parser will |
+# explode, so don't do it. |
+attrfind = re.compile( |
+ r'\s*([a-zA-Z_][-.:a-zA-Z_0-9]*)(\s*=\s*' |
+ r'(\'[^\']*\'|"[^"]*"|[^\s"\'=<>`]*))?') |
+attrfind_tolerant = re.compile( |
+ r'((?<=[\'"\s/])[^\s/>][^\s/=>]*)(\s*=+\s*' |
+ r'(\'[^\']*\'|"[^"]*"|(?![\'"])[^>\s]*))?(?:\s|/(?!>))*') |
+locatestarttagend = re.compile(r""" |
+ <[a-zA-Z][-.a-zA-Z0-9:_]* # tag name |
+ (?:\s+ # whitespace before attribute name |
+ (?:[a-zA-Z_][-.:a-zA-Z0-9_]* # attribute name |
+ (?:\s*=\s* # value indicator |
+ (?:'[^']*' # LITA-enclosed value |
+ |\"[^\"]*\" # LIT-enclosed value |
+ |[^'\">\s]+ # bare value |
+ ) |
+ )? |
+ ) |
+ )* |
+ \s* # trailing whitespace |
+""", re.VERBOSE) |
+locatestarttagend_tolerant = re.compile(r""" |
+ <[a-zA-Z][-.a-zA-Z0-9:_]* # tag name |
+ (?:[\s/]* # optional whitespace before attribute name |
+ (?:(?<=['"\s/])[^\s/>][^\s/=>]* # attribute name |
+ (?:\s*=+\s* # value indicator |
+ (?:'[^']*' # LITA-enclosed value |
+ |"[^"]*" # LIT-enclosed value |
+ |(?!['"])[^>\s]* # bare value |
+ ) |
+ (?:\s*,)* # possibly followed by a comma |
+ )?(?:\s|/(?!>))* |
+ )* |
+ )? |
+ \s* # trailing whitespace |
+""", re.VERBOSE) |
+endendtag = re.compile('>') |
+# the HTML 5 spec, section 8.1.2.2, doesn't allow spaces between |
+# </ and the tag name, so maybe this should be fixed |
+endtagfind = re.compile('</\s*([a-zA-Z][-.a-zA-Z0-9:_]*)\s*>') |
+ |
+ |
+class HTMLParseError(Exception): |
+ """Exception raised for all parse errors.""" |
+ |
+ def __init__(self, msg, position=(None, None)): |
+ assert msg |
+ self.msg = msg |
+ self.lineno = position[0] |
+ self.offset = position[1] |
+ |
+ def __str__(self): |
+ result = self.msg |
+ if self.lineno is not None: |
+ result = result + ", at line %d" % self.lineno |
+ if self.offset is not None: |
+ result = result + ", column %d" % (self.offset + 1) |
+ return result |
+ |
+ |
+class HTMLParser(_markupbase.ParserBase): |
+ """Find tags and other markup and call handler functions. |
+ |
+ Usage: |
+ p = HTMLParser() |
+ p.feed(data) |
+ ... |
+ p.close() |
+ |
+ Start tags are handled by calling self.handle_starttag() or |
+ self.handle_startendtag(); end tags by self.handle_endtag(). The |
+ data between tags is passed from the parser to the derived class |
+ by calling self.handle_data() with the data as argument (the data |
+ may be split up in arbitrary chunks). Entity references are |
+ passed by calling self.handle_entityref() with the entity |
+ reference as the argument. Numeric character references are |
+ passed to self.handle_charref() with the string containing the |
+ reference as the argument. |
+ """ |
+ |
+ CDATA_CONTENT_ELEMENTS = ("script", "style") |
+ |
+ def __init__(self, strict=False): |
+ """Initialize and reset this instance. |
+ |
+ If strict is set to False (the default) the parser will parse invalid |
+ markup, otherwise it will raise an error. Note that the strict mode |
+ is deprecated. |
+ """ |
+ if strict: |
+ warnings.warn("The strict mode is deprecated.", |
+ DeprecationWarning, stacklevel=2) |
+ self.strict = strict |
+ self.reset() |
+ |
+ def reset(self): |
+ """Reset this instance. Loses all unprocessed data.""" |
+ self.rawdata = '' |
+ self.lasttag = '???' |
+ self.interesting = interesting_normal |
+ self.cdata_elem = None |
+ _markupbase.ParserBase.reset(self) |
+ |
+ def feed(self, data): |
+ r"""Feed data to the parser. |
+ |
+ Call this as often as you want, with as little or as much text |
+ as you want (may include '\n'). |
+ """ |
+ self.rawdata = self.rawdata + data |
+ self.goahead(0) |
+ |
+ def close(self): |
+ """Handle any buffered data.""" |
+ self.goahead(1) |
+ |
+ def error(self, message): |
+ raise HTMLParseError(message, self.getpos()) |
+ |
+ __starttag_text = None |
+ |
+ def get_starttag_text(self): |
+ """Return full source of start tag: '<...>'.""" |
+ return self.__starttag_text |
+ |
+ def set_cdata_mode(self, elem): |
+ self.cdata_elem = elem.lower() |
+ self.interesting = re.compile(r'</\s*%s\s*>' % self.cdata_elem, re.I) |
+ |
+ def clear_cdata_mode(self): |
+ self.interesting = interesting_normal |
+ self.cdata_elem = None |
+ |
+ # 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: |
+ match = self.interesting.search(rawdata, i) # < or & |
+ if match: |
+ j = match.start() |
+ else: |
+ if self.cdata_elem: |
+ break |
+ j = n |
+ if i < j: self.handle_data(rawdata[i:j]) |
+ i = self.updatepos(i, j) |
+ if i == n: break |
+ startswith = rawdata.startswith |
+ if startswith('<', i): |
+ if starttagopen.match(rawdata, i): # < + letter |
+ k = self.parse_starttag(i) |
+ elif startswith("</", i): |
+ k = self.parse_endtag(i) |
+ elif startswith("<!--", i): |
+ k = self.parse_comment(i) |
+ elif startswith("<?", i): |
+ k = self.parse_pi(i) |
+ elif startswith("<!", i): |
+ if self.strict: |
+ k = self.parse_declaration(i) |
+ else: |
+ k = self.parse_html_declaration(i) |
+ elif (i + 1) < n: |
+ self.handle_data("<") |
+ k = i + 1 |
+ else: |
+ break |
+ if k < 0: |
+ if not end: |
+ break |
+ if self.strict: |
+ self.error("EOF in middle of construct") |
+ k = rawdata.find('>', i + 1) |
+ if k < 0: |
+ k = rawdata.find('<', i + 1) |
+ if k < 0: |
+ k = i + 1 |
+ else: |
+ k += 1 |
+ self.handle_data(rawdata[i:k]) |
+ i = self.updatepos(i, k) |
+ elif startswith("&#", i): |
+ match = charref.match(rawdata, i) |
+ if match: |
+ name = match.group()[2:-1] |
+ self.handle_charref(name) |
+ k = match.end() |
+ if not startswith(';', k-1): |
+ k = k - 1 |
+ i = self.updatepos(i, k) |
+ continue |
+ else: |
+ if ";" in rawdata[i:]: #bail by consuming &# |
+ self.handle_data(rawdata[0:2]) |
+ i = self.updatepos(i, 2) |
+ break |
+ elif startswith('&', i): |
+ match = entityref.match(rawdata, i) |
+ if match: |
+ name = match.group(1) |
+ self.handle_entityref(name) |
+ k = match.end() |
+ if not startswith(';', k-1): |
+ k = k - 1 |
+ i = self.updatepos(i, k) |
+ continue |
+ match = incomplete.match(rawdata, i) |
+ if match: |
+ # match.group() will contain at least 2 chars |
+ if end and match.group() == rawdata[i:]: |
+ if self.strict: |
+ self.error("EOF in middle of entity or char ref") |
+ else: |
+ if k <= i: |
+ k = n |
+ i = self.updatepos(i, i + 1) |
+ # incomplete |
+ break |
+ elif (i + 1) < n: |
+ # not the end of the buffer, and can't be confused |
+ # with some other construct |
+ self.handle_data("&") |
+ i = self.updatepos(i, i + 1) |
+ else: |
+ break |
+ else: |
+ assert 0, "interesting.search() lied" |
+ # end while |
+ if end and i < n and not self.cdata_elem: |
+ self.handle_data(rawdata[i:n]) |
+ i = self.updatepos(i, n) |
+ self.rawdata = rawdata[i:] |
+ |
+ # Internal -- parse html declarations, return length or -1 if not terminated |
+ # See w3.org/TR/html5/tokenization.html#markup-declaration-open-state |
+ # See also parse_declaration in _markupbase |
+ def parse_html_declaration(self, i): |
+ rawdata = self.rawdata |
+ assert rawdata[i:i+2] == '<!', ('unexpected call to ' |
+ 'parse_html_declaration()') |
+ if rawdata[i:i+4] == '<!--': |
+ # this case is actually already handled in goahead() |
+ return self.parse_comment(i) |
+ elif rawdata[i:i+3] == '<![': |
+ return self.parse_marked_section(i) |
+ elif rawdata[i:i+9].lower() == '<!doctype': |
+ # find the closing > |
+ gtpos = rawdata.find('>', i+9) |
+ if gtpos == -1: |
+ return -1 |
+ self.handle_decl(rawdata[i+2:gtpos]) |
+ return gtpos+1 |
+ else: |
+ return self.parse_bogus_comment(i) |
+ |
+ # Internal -- parse bogus comment, return length or -1 if not terminated |
+ # see http://www.w3.org/TR/html5/tokenization.html#bogus-comment-state |
+ def parse_bogus_comment(self, i, report=1): |
+ rawdata = self.rawdata |
+ assert rawdata[i:i+2] in ('<!', '</'), ('unexpected call to ' |
+ 'parse_comment()') |
+ pos = rawdata.find('>', i+2) |
+ if pos == -1: |
+ return -1 |
+ if report: |
+ self.handle_comment(rawdata[i+2:pos]) |
+ return pos + 1 |
+ |
+ # Internal -- parse processing instr, return end or -1 if not terminated |
+ def parse_pi(self, i): |
+ rawdata = self.rawdata |
+ assert rawdata[i:i+2] == '<?', 'unexpected call to parse_pi()' |
+ match = piclose.search(rawdata, i+2) # > |
+ if not match: |
+ return -1 |
+ j = match.start() |
+ self.handle_pi(rawdata[i+2: j]) |
+ j = match.end() |
+ return j |
+ |
+ # Internal -- handle starttag, return end or -1 if not terminated |
+ def parse_starttag(self, i): |
+ self.__starttag_text = None |
+ endpos = self.check_for_whole_start_tag(i) |
+ if endpos < 0: |
+ return endpos |
+ rawdata = self.rawdata |
+ self.__starttag_text = rawdata[i:endpos] |
+ |
+ # Now parse the data between i+1 and j into a tag and attrs |
+ attrs = [] |
+ match = tagfind.match(rawdata, i+1) |
+ assert match, 'unexpected call to parse_starttag()' |
+ k = match.end() |
+ self.lasttag = tag = match.group(1).lower() |
+ while k < endpos: |
+ if self.strict: |
+ m = attrfind.match(rawdata, k) |
+ else: |
+ m = attrfind_tolerant.match(rawdata, k) |
+ if not m: |
+ break |
+ attrname, rest, attrvalue = m.group(1, 2, 3) |
+ if not rest: |
+ attrvalue = None |
+ elif attrvalue[:1] == '\'' == attrvalue[-1:] or \ |
+ attrvalue[:1] == '"' == attrvalue[-1:]: |
+ attrvalue = attrvalue[1:-1] |
+ if attrvalue: |
+ attrvalue = self.unescape(attrvalue) |
+ attrs.append((attrname.lower(), attrvalue)) |
+ k = m.end() |
+ |
+ end = rawdata[k:endpos].strip() |
+ if end not in (">", "/>"): |
+ lineno, offset = self.getpos() |
+ if "\n" in self.__starttag_text: |
+ lineno = lineno + self.__starttag_text.count("\n") |
+ offset = len(self.__starttag_text) \ |
+ - self.__starttag_text.rfind("\n") |
+ else: |
+ offset = offset + len(self.__starttag_text) |
+ if self.strict: |
+ self.error("junk characters in start tag: %r" |
+ % (rawdata[k:endpos][:20],)) |
+ self.handle_data(rawdata[i:endpos]) |
+ return endpos |
+ if end.endswith('/>'): |
+ # XHTML-style empty tag: <span attr="value" /> |
+ self.handle_startendtag(tag, attrs) |
+ else: |
+ self.handle_starttag(tag, attrs) |
+ if tag in self.CDATA_CONTENT_ELEMENTS: |
+ self.set_cdata_mode(tag) |
+ return endpos |
+ |
+ # Internal -- check to see if we have a complete starttag; return end |
+ # or -1 if incomplete. |
+ def check_for_whole_start_tag(self, i): |
+ rawdata = self.rawdata |
+ if self.strict: |
+ m = locatestarttagend.match(rawdata, i) |
+ else: |
+ m = locatestarttagend_tolerant.match(rawdata, i) |
+ if m: |
+ j = m.end() |
+ next = rawdata[j:j+1] |
+ if next == ">": |
+ return j + 1 |
+ if next == "/": |
+ if rawdata.startswith("/>", j): |
+ return j + 2 |
+ if rawdata.startswith("/", j): |
+ # buffer boundary |
+ return -1 |
+ # else bogus input |
+ if self.strict: |
+ self.updatepos(i, j + 1) |
+ self.error("malformed empty start tag") |
+ if j > i: |
+ return j |
+ else: |
+ return i + 1 |
+ if next == "": |
+ # end of input |
+ return -1 |
+ if next in ("abcdefghijklmnopqrstuvwxyz=/" |
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"): |
+ # end of input in or before attribute value, or we have the |
+ # '/' from a '/>' ending |
+ return -1 |
+ if self.strict: |
+ self.updatepos(i, j) |
+ self.error("malformed start tag") |
+ if j > i: |
+ return j |
+ else: |
+ return i + 1 |
+ raise AssertionError("we should not get here!") |
+ |
+ # Internal -- parse endtag, return end or -1 if incomplete |
+ def parse_endtag(self, i): |
+ rawdata = self.rawdata |
+ assert rawdata[i:i+2] == "</", "unexpected call to parse_endtag" |
+ match = endendtag.search(rawdata, i+1) # > |
+ if not match: |
+ return -1 |
+ gtpos = match.end() |
+ match = endtagfind.match(rawdata, i) # </ + tag + > |
+ if not match: |
+ if self.cdata_elem is not None: |
+ self.handle_data(rawdata[i:gtpos]) |
+ return gtpos |
+ if self.strict: |
+ self.error("bad end tag: %r" % (rawdata[i:gtpos],)) |
+ # find the name: w3.org/TR/html5/tokenization.html#tag-name-state |
+ namematch = tagfind_tolerant.match(rawdata, i+2) |
+ if not namematch: |
+ # w3.org/TR/html5/tokenization.html#end-tag-open-state |
+ if rawdata[i:i+3] == '</>': |
+ return i+3 |
+ else: |
+ return self.parse_bogus_comment(i) |
+ tagname = namematch.group().lower() |
+ # consume and ignore other stuff between the name and the > |
+ # Note: this is not 100% correct, since we might have things like |
+ # </tag attr=">">, but looking for > after tha name should cover |
+ # most of the cases and is much simpler |
+ gtpos = rawdata.find('>', namematch.end()) |
+ self.handle_endtag(tagname) |
+ return gtpos+1 |
+ |
+ elem = match.group(1).lower() # script or style |
+ if self.cdata_elem is not None: |
+ if elem != self.cdata_elem: |
+ self.handle_data(rawdata[i:gtpos]) |
+ return gtpos |
+ |
+ self.handle_endtag(elem.lower()) |
+ self.clear_cdata_mode() |
+ return gtpos |
+ |
+ # Overridable -- finish processing of start+end tag: <tag.../> |
+ def handle_startendtag(self, tag, attrs): |
+ self.handle_starttag(tag, attrs) |
+ self.handle_endtag(tag) |
+ |
+ # Overridable -- handle start tag |
+ def handle_starttag(self, tag, attrs): |
+ pass |
+ |
+ # Overridable -- handle end tag |
+ def handle_endtag(self, tag): |
+ pass |
+ |
+ # Overridable -- handle character reference |
+ def handle_charref(self, name): |
+ pass |
+ |
+ # Overridable -- handle entity reference |
+ def handle_entityref(self, name): |
+ pass |
+ |
+ # Overridable -- handle data |
+ def handle_data(self, data): |
+ pass |
+ |
+ # Overridable -- handle comment |
+ def handle_comment(self, data): |
+ pass |
+ |
+ # Overridable -- handle declaration |
+ def handle_decl(self, decl): |
+ pass |
+ |
+ # Overridable -- handle processing instruction |
+ def handle_pi(self, data): |
+ pass |
+ |
+ def unknown_decl(self, data): |
+ if self.strict: |
+ self.error("unknown declaration: %r" % (data,)) |
+ |
+ # Internal -- helper to remove special character quoting |
+ def unescape(self, s): |
+ if '&' not in s: |
+ return s |
+ def replaceEntities(s): |
+ s = s.groups()[0] |
+ try: |
+ if s[0] == "#": |
+ s = s[1:] |
+ if s[0] in ['x','X']: |
+ c = int(s[1:].rstrip(';'), 16) |
+ else: |
+ c = int(s.rstrip(';')) |
+ return chr(c) |
+ except ValueError: |
+ return '&#' + s |
+ else: |
+ from future.backports.html.entities import html5 |
+ if s in html5: |
+ return html5[s] |
+ elif s.endswith(';'): |
+ return '&' + s |
+ for x in range(2, len(s)): |
+ if s[:x] in html5: |
+ return html5[s[:x]] + s[x:] |
+ else: |
+ return '&' + s |
+ |
+ return re.sub(r"&(#?[xX]?(?:[0-9a-fA-F]+;|\w{1,32};?))", |
+ replaceEntities, s) |
+ |