_markupbase.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. """Shared support for scanning document type declarations in HTML and XHTML.
  2. Backported for python-future from Python 3.3. Reason: ParserBase is an
  3. old-style class in the Python 2.7 source of markupbase.py, which I suspect
  4. might be the cause of sporadic unit-test failures on travis-ci.org with
  5. test_htmlparser.py. The test failures look like this:
  6. ======================================================================
  7. ERROR: test_attr_entity_replacement (future.tests.test_htmlparser.AttributesStrictTestCase)
  8. ----------------------------------------------------------------------
  9. Traceback (most recent call last):
  10. File "/home/travis/build/edschofield/python-future/future/tests/test_htmlparser.py", line 661, in test_attr_entity_replacement
  11. [("starttag", "a", [("b", "&><\"'")])])
  12. File "/home/travis/build/edschofield/python-future/future/tests/test_htmlparser.py", line 93, in _run_check
  13. collector = self.get_collector()
  14. File "/home/travis/build/edschofield/python-future/future/tests/test_htmlparser.py", line 617, in get_collector
  15. return EventCollector(strict=True)
  16. File "/home/travis/build/edschofield/python-future/future/tests/test_htmlparser.py", line 27, in __init__
  17. html.parser.HTMLParser.__init__(self, *args, **kw)
  18. File "/home/travis/build/edschofield/python-future/future/backports/html/parser.py", line 135, in __init__
  19. self.reset()
  20. File "/home/travis/build/edschofield/python-future/future/backports/html/parser.py", line 143, in reset
  21. _markupbase.ParserBase.reset(self)
  22. TypeError: unbound method reset() must be called with ParserBase instance as first argument (got EventCollector instance instead)
  23. This module is used as a foundation for the html.parser module. It has no
  24. documented public API and should not be used directly.
  25. """
  26. import re
  27. _declname_match = re.compile(r'[a-zA-Z][-_.a-zA-Z0-9]*\s*').match
  28. _declstringlit_match = re.compile(r'(\'[^\']*\'|"[^"]*")\s*').match
  29. _commentclose = re.compile(r'--\s*>')
  30. _markedsectionclose = re.compile(r']\s*]\s*>')
  31. # An analysis of the MS-Word extensions is available at
  32. # http://www.planetpublish.com/xmlarena/xap/Thursday/WordtoXML.pdf
  33. _msmarkedsectionclose = re.compile(r']\s*>')
  34. del re
  35. class ParserBase(object):
  36. """Parser base class which provides some common support methods used
  37. by the SGML/HTML and XHTML parsers."""
  38. def __init__(self):
  39. if self.__class__ is ParserBase:
  40. raise RuntimeError(
  41. "_markupbase.ParserBase must be subclassed")
  42. def error(self, message):
  43. raise NotImplementedError(
  44. "subclasses of ParserBase must override error()")
  45. def reset(self):
  46. self.lineno = 1
  47. self.offset = 0
  48. def getpos(self):
  49. """Return current line number and offset."""
  50. return self.lineno, self.offset
  51. # Internal -- update line number and offset. This should be
  52. # called for each piece of data exactly once, in order -- in other
  53. # words the concatenation of all the input strings to this
  54. # function should be exactly the entire input.
  55. def updatepos(self, i, j):
  56. if i >= j:
  57. return j
  58. rawdata = self.rawdata
  59. nlines = rawdata.count("\n", i, j)
  60. if nlines:
  61. self.lineno = self.lineno + nlines
  62. pos = rawdata.rindex("\n", i, j) # Should not fail
  63. self.offset = j-(pos+1)
  64. else:
  65. self.offset = self.offset + j-i
  66. return j
  67. _decl_otherchars = ''
  68. # Internal -- parse declaration (for use by subclasses).
  69. def parse_declaration(self, i):
  70. # This is some sort of declaration; in "HTML as
  71. # deployed," this should only be the document type
  72. # declaration ("<!DOCTYPE html...>").
  73. # ISO 8879:1986, however, has more complex
  74. # declaration syntax for elements in <!...>, including:
  75. # --comment--
  76. # [marked section]
  77. # name in the following list: ENTITY, DOCTYPE, ELEMENT,
  78. # ATTLIST, NOTATION, SHORTREF, USEMAP,
  79. # LINKTYPE, LINK, IDLINK, USELINK, SYSTEM
  80. rawdata = self.rawdata
  81. j = i + 2
  82. assert rawdata[i:j] == "<!", "unexpected call to parse_declaration"
  83. if rawdata[j:j+1] == ">":
  84. # the empty comment <!>
  85. return j + 1
  86. if rawdata[j:j+1] in ("-", ""):
  87. # Start of comment followed by buffer boundary,
  88. # or just a buffer boundary.
  89. return -1
  90. # A simple, practical version could look like: ((name|stringlit) S*) + '>'
  91. n = len(rawdata)
  92. if rawdata[j:j+2] == '--': #comment
  93. # Locate --.*-- as the body of the comment
  94. return self.parse_comment(i)
  95. elif rawdata[j] == '[': #marked section
  96. # Locate [statusWord [...arbitrary SGML...]] as the body of the marked section
  97. # Where statusWord is one of TEMP, CDATA, IGNORE, INCLUDE, RCDATA
  98. # Note that this is extended by Microsoft Office "Save as Web" function
  99. # to include [if...] and [endif].
  100. return self.parse_marked_section(i)
  101. else: #all other declaration elements
  102. decltype, j = self._scan_name(j, i)
  103. if j < 0:
  104. return j
  105. if decltype == "doctype":
  106. self._decl_otherchars = ''
  107. while j < n:
  108. c = rawdata[j]
  109. if c == ">":
  110. # end of declaration syntax
  111. data = rawdata[i+2:j]
  112. if decltype == "doctype":
  113. self.handle_decl(data)
  114. else:
  115. # According to the HTML5 specs sections "8.2.4.44 Bogus
  116. # comment state" and "8.2.4.45 Markup declaration open
  117. # state", a comment token should be emitted.
  118. # Calling unknown_decl provides more flexibility though.
  119. self.unknown_decl(data)
  120. return j + 1
  121. if c in "\"'":
  122. m = _declstringlit_match(rawdata, j)
  123. if not m:
  124. return -1 # incomplete
  125. j = m.end()
  126. elif c in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ":
  127. name, j = self._scan_name(j, i)
  128. elif c in self._decl_otherchars:
  129. j = j + 1
  130. elif c == "[":
  131. # this could be handled in a separate doctype parser
  132. if decltype == "doctype":
  133. j = self._parse_doctype_subset(j + 1, i)
  134. elif decltype in set(["attlist", "linktype", "link", "element"]):
  135. # must tolerate []'d groups in a content model in an element declaration
  136. # also in data attribute specifications of attlist declaration
  137. # also link type declaration subsets in linktype declarations
  138. # also link attribute specification lists in link declarations
  139. self.error("unsupported '[' char in %s declaration" % decltype)
  140. else:
  141. self.error("unexpected '[' char in declaration")
  142. else:
  143. self.error(
  144. "unexpected %r char in declaration" % rawdata[j])
  145. if j < 0:
  146. return j
  147. return -1 # incomplete
  148. # Internal -- parse a marked section
  149. # Override this to handle MS-word extension syntax <![if word]>content<![endif]>
  150. def parse_marked_section(self, i, report=1):
  151. rawdata= self.rawdata
  152. assert rawdata[i:i+3] == '<![', "unexpected call to parse_marked_section()"
  153. sectName, j = self._scan_name( i+3, i )
  154. if j < 0:
  155. return j
  156. if sectName in set(["temp", "cdata", "ignore", "include", "rcdata"]):
  157. # look for standard ]]> ending
  158. match= _markedsectionclose.search(rawdata, i+3)
  159. elif sectName in set(["if", "else", "endif"]):
  160. # look for MS Office ]> ending
  161. match= _msmarkedsectionclose.search(rawdata, i+3)
  162. else:
  163. self.error('unknown status keyword %r in marked section' % rawdata[i+3:j])
  164. if not match:
  165. return -1
  166. if report:
  167. j = match.start(0)
  168. self.unknown_decl(rawdata[i+3: j])
  169. return match.end(0)
  170. # Internal -- parse comment, return length or -1 if not terminated
  171. def parse_comment(self, i, report=1):
  172. rawdata = self.rawdata
  173. if rawdata[i:i+4] != '<!--':
  174. self.error('unexpected call to parse_comment()')
  175. match = _commentclose.search(rawdata, i+4)
  176. if not match:
  177. return -1
  178. if report:
  179. j = match.start(0)
  180. self.handle_comment(rawdata[i+4: j])
  181. return match.end(0)
  182. # Internal -- scan past the internal subset in a <!DOCTYPE declaration,
  183. # returning the index just past any whitespace following the trailing ']'.
  184. def _parse_doctype_subset(self, i, declstartpos):
  185. rawdata = self.rawdata
  186. n = len(rawdata)
  187. j = i
  188. while j < n:
  189. c = rawdata[j]
  190. if c == "<":
  191. s = rawdata[j:j+2]
  192. if s == "<":
  193. # end of buffer; incomplete
  194. return -1
  195. if s != "<!":
  196. self.updatepos(declstartpos, j + 1)
  197. self.error("unexpected char in internal subset (in %r)" % s)
  198. if (j + 2) == n:
  199. # end of buffer; incomplete
  200. return -1
  201. if (j + 4) > n:
  202. # end of buffer; incomplete
  203. return -1
  204. if rawdata[j:j+4] == "<!--":
  205. j = self.parse_comment(j, report=0)
  206. if j < 0:
  207. return j
  208. continue
  209. name, j = self._scan_name(j + 2, declstartpos)
  210. if j == -1:
  211. return -1
  212. if name not in set(["attlist", "element", "entity", "notation"]):
  213. self.updatepos(declstartpos, j + 2)
  214. self.error(
  215. "unknown declaration %r in internal subset" % name)
  216. # handle the individual names
  217. meth = getattr(self, "_parse_doctype_" + name)
  218. j = meth(j, declstartpos)
  219. if j < 0:
  220. return j
  221. elif c == "%":
  222. # parameter entity reference
  223. if (j + 1) == n:
  224. # end of buffer; incomplete
  225. return -1
  226. s, j = self._scan_name(j + 1, declstartpos)
  227. if j < 0:
  228. return j
  229. if rawdata[j] == ";":
  230. j = j + 1
  231. elif c == "]":
  232. j = j + 1
  233. while j < n and rawdata[j].isspace():
  234. j = j + 1
  235. if j < n:
  236. if rawdata[j] == ">":
  237. return j
  238. self.updatepos(declstartpos, j)
  239. self.error("unexpected char after internal subset")
  240. else:
  241. return -1
  242. elif c.isspace():
  243. j = j + 1
  244. else:
  245. self.updatepos(declstartpos, j)
  246. self.error("unexpected char %r in internal subset" % c)
  247. # end of buffer reached
  248. return -1
  249. # Internal -- scan past <!ELEMENT declarations
  250. def _parse_doctype_element(self, i, declstartpos):
  251. name, j = self._scan_name(i, declstartpos)
  252. if j == -1:
  253. return -1
  254. # style content model; just skip until '>'
  255. rawdata = self.rawdata
  256. if '>' in rawdata[j:]:
  257. return rawdata.find(">", j) + 1
  258. return -1
  259. # Internal -- scan past <!ATTLIST declarations
  260. def _parse_doctype_attlist(self, i, declstartpos):
  261. rawdata = self.rawdata
  262. name, j = self._scan_name(i, declstartpos)
  263. c = rawdata[j:j+1]
  264. if c == "":
  265. return -1
  266. if c == ">":
  267. return j + 1
  268. while 1:
  269. # scan a series of attribute descriptions; simplified:
  270. # name type [value] [#constraint]
  271. name, j = self._scan_name(j, declstartpos)
  272. if j < 0:
  273. return j
  274. c = rawdata[j:j+1]
  275. if c == "":
  276. return -1
  277. if c == "(":
  278. # an enumerated type; look for ')'
  279. if ")" in rawdata[j:]:
  280. j = rawdata.find(")", j) + 1
  281. else:
  282. return -1
  283. while rawdata[j:j+1].isspace():
  284. j = j + 1
  285. if not rawdata[j:]:
  286. # end of buffer, incomplete
  287. return -1
  288. else:
  289. name, j = self._scan_name(j, declstartpos)
  290. c = rawdata[j:j+1]
  291. if not c:
  292. return -1
  293. if c in "'\"":
  294. m = _declstringlit_match(rawdata, j)
  295. if m:
  296. j = m.end()
  297. else:
  298. return -1
  299. c = rawdata[j:j+1]
  300. if not c:
  301. return -1
  302. if c == "#":
  303. if rawdata[j:] == "#":
  304. # end of buffer
  305. return -1
  306. name, j = self._scan_name(j + 1, declstartpos)
  307. if j < 0:
  308. return j
  309. c = rawdata[j:j+1]
  310. if not c:
  311. return -1
  312. if c == '>':
  313. # all done
  314. return j + 1
  315. # Internal -- scan past <!NOTATION declarations
  316. def _parse_doctype_notation(self, i, declstartpos):
  317. name, j = self._scan_name(i, declstartpos)
  318. if j < 0:
  319. return j
  320. rawdata = self.rawdata
  321. while 1:
  322. c = rawdata[j:j+1]
  323. if not c:
  324. # end of buffer; incomplete
  325. return -1
  326. if c == '>':
  327. return j + 1
  328. if c in "'\"":
  329. m = _declstringlit_match(rawdata, j)
  330. if not m:
  331. return -1
  332. j = m.end()
  333. else:
  334. name, j = self._scan_name(j, declstartpos)
  335. if j < 0:
  336. return j
  337. # Internal -- scan past <!ENTITY declarations
  338. def _parse_doctype_entity(self, i, declstartpos):
  339. rawdata = self.rawdata
  340. if rawdata[i:i+1] == "%":
  341. j = i + 1
  342. while 1:
  343. c = rawdata[j:j+1]
  344. if not c:
  345. return -1
  346. if c.isspace():
  347. j = j + 1
  348. else:
  349. break
  350. else:
  351. j = i
  352. name, j = self._scan_name(j, declstartpos)
  353. if j < 0:
  354. return j
  355. while 1:
  356. c = self.rawdata[j:j+1]
  357. if not c:
  358. return -1
  359. if c in "'\"":
  360. m = _declstringlit_match(rawdata, j)
  361. if m:
  362. j = m.end()
  363. else:
  364. return -1 # incomplete
  365. elif c == ">":
  366. return j + 1
  367. else:
  368. name, j = self._scan_name(j, declstartpos)
  369. if j < 0:
  370. return j
  371. # Internal -- scan a name token and the new position and the token, or
  372. # return -1 if we've reached the end of the buffer.
  373. def _scan_name(self, i, declstartpos):
  374. rawdata = self.rawdata
  375. n = len(rawdata)
  376. if i == n:
  377. return None, -1
  378. m = _declname_match(rawdata, i)
  379. if m:
  380. s = m.group()
  381. name = s.strip()
  382. if (i + len(s)) == n:
  383. return None, -1 # end of buffer
  384. return name.lower(), m.end()
  385. else:
  386. self.updatepos(declstartpos, i)
  387. self.error("expected name token at %r"
  388. % rawdata[declstartpos:declstartpos+20])
  389. # To be overridden -- handlers for unknown objects
  390. def unknown_decl(self, data):
  391. pass