_markupbase.py 14 KB

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