_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 error(self, message):
  22. raise NotImplementedError(
  23. "subclasses of ParserBase must override error()")
  24. def reset(self):
  25. self.lineno = 1
  26. self.offset = 0
  27. def getpos(self):
  28. """Return current line number and offset."""
  29. return self.lineno, self.offset
  30. # Internal -- update line number and offset. This should be
  31. # called for each piece of data exactly once, in order -- in other
  32. # words the concatenation of all the input strings to this
  33. # function should be exactly the entire input.
  34. def updatepos(self, i, j):
  35. if i >= j:
  36. return j
  37. rawdata = self.rawdata
  38. nlines = rawdata.count("\n", i, j)
  39. if nlines:
  40. self.lineno = self.lineno + nlines
  41. pos = rawdata.rindex("\n", i, j) # Should not fail
  42. self.offset = j-(pos+1)
  43. else:
  44. self.offset = self.offset + j-i
  45. return j
  46. _decl_otherchars = ''
  47. # Internal -- parse declaration (for use by subclasses).
  48. def parse_declaration(self, i):
  49. # This is some sort of declaration; in "HTML as
  50. # deployed," this should only be the document type
  51. # declaration ("<!DOCTYPE html...>").
  52. # ISO 8879:1986, however, has more complex
  53. # declaration syntax for elements in <!...>, including:
  54. # --comment--
  55. # [marked section]
  56. # name in the following list: ENTITY, DOCTYPE, ELEMENT,
  57. # ATTLIST, NOTATION, SHORTREF, USEMAP,
  58. # LINKTYPE, LINK, IDLINK, USELINK, SYSTEM
  59. rawdata = self.rawdata
  60. j = i + 2
  61. assert rawdata[i:j] == "<!", "unexpected call to parse_declaration"
  62. if rawdata[j:j+1] == ">":
  63. # the empty comment <!>
  64. return j + 1
  65. if rawdata[j:j+1] in ("-", ""):
  66. # Start of comment followed by buffer boundary,
  67. # or just a buffer boundary.
  68. return -1
  69. # A simple, practical version could look like: ((name|stringlit) S*) + '>'
  70. n = len(rawdata)
  71. if rawdata[j:j+2] == '--': #comment
  72. # Locate --.*-- as the body of the comment
  73. return self.parse_comment(i)
  74. elif rawdata[j] == '[': #marked section
  75. # Locate [statusWord [...arbitrary SGML...]] as the body of the marked section
  76. # Where statusWord is one of TEMP, CDATA, IGNORE, INCLUDE, RCDATA
  77. # Note that this is extended by Microsoft Office "Save as Web" function
  78. # to include [if...] and [endif].
  79. return self.parse_marked_section(i)
  80. else: #all other declaration elements
  81. decltype, j = self._scan_name(j, i)
  82. if j < 0:
  83. return j
  84. if decltype == "doctype":
  85. self._decl_otherchars = ''
  86. while j < n:
  87. c = rawdata[j]
  88. if c == ">":
  89. # end of declaration syntax
  90. data = rawdata[i+2:j]
  91. if decltype == "doctype":
  92. self.handle_decl(data)
  93. else:
  94. # According to the HTML5 specs sections "8.2.4.44 Bogus
  95. # comment state" and "8.2.4.45 Markup declaration open
  96. # state", a comment token should be emitted.
  97. # Calling unknown_decl provides more flexibility though.
  98. self.unknown_decl(data)
  99. return j + 1
  100. if c in "\"'":
  101. m = _declstringlit_match(rawdata, j)
  102. if not m:
  103. return -1 # incomplete
  104. j = m.end()
  105. elif c in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ":
  106. name, j = self._scan_name(j, i)
  107. elif c in self._decl_otherchars:
  108. j = j + 1
  109. elif c == "[":
  110. # this could be handled in a separate doctype parser
  111. if decltype == "doctype":
  112. j = self._parse_doctype_subset(j + 1, i)
  113. elif decltype in {"attlist", "linktype", "link", "element"}:
  114. # must tolerate []'d groups in a content model in an element declaration
  115. # also in data attribute specifications of attlist declaration
  116. # also link type declaration subsets in linktype declarations
  117. # also link attribute specification lists in link declarations
  118. self.error("unsupported '[' char in %s declaration" % decltype)
  119. else:
  120. self.error("unexpected '[' char in declaration")
  121. else:
  122. self.error(
  123. "unexpected %r char in declaration" % rawdata[j])
  124. if j < 0:
  125. return j
  126. return -1 # incomplete
  127. # Internal -- parse a marked section
  128. # Override this to handle MS-word extension syntax <![if word]>content<![endif]>
  129. def parse_marked_section(self, i, report=1):
  130. rawdata= self.rawdata
  131. assert rawdata[i:i+3] == '<![', "unexpected call to parse_marked_section()"
  132. sectName, j = self._scan_name( i+3, i )
  133. if j < 0:
  134. return j
  135. if sectName in {"temp", "cdata", "ignore", "include", "rcdata"}:
  136. # look for standard ]]> ending
  137. match= _markedsectionclose.search(rawdata, i+3)
  138. elif sectName in {"if", "else", "endif"}:
  139. # look for MS Office ]> ending
  140. match= _msmarkedsectionclose.search(rawdata, i+3)
  141. else:
  142. self.error('unknown status keyword %r in marked section' % rawdata[i+3:j])
  143. match = None
  144. if not match:
  145. return -1
  146. if report:
  147. j = match.start(0)
  148. self.unknown_decl(rawdata[i+3: j])
  149. return match.end(0)
  150. # Internal -- parse comment, return length or -1 if not terminated
  151. def parse_comment(self, i, report=1):
  152. rawdata = self.rawdata
  153. if rawdata[i:i+4] != '<!--':
  154. self.error('unexpected call to parse_comment()')
  155. match = _commentclose.search(rawdata, i+4)
  156. if not match:
  157. return -1
  158. if report:
  159. j = match.start(0)
  160. self.handle_comment(rawdata[i+4: j])
  161. return match.end(0)
  162. # Internal -- scan past the internal subset in a <!DOCTYPE declaration,
  163. # returning the index just past any whitespace following the trailing ']'.
  164. def _parse_doctype_subset(self, i, declstartpos):
  165. rawdata = self.rawdata
  166. n = len(rawdata)
  167. j = i
  168. while j < n:
  169. c = rawdata[j]
  170. if c == "<":
  171. s = rawdata[j:j+2]
  172. if s == "<":
  173. # end of buffer; incomplete
  174. return -1
  175. if s != "<!":
  176. self.updatepos(declstartpos, j + 1)
  177. self.error("unexpected char in internal subset (in %r)" % s)
  178. if (j + 2) == n:
  179. # end of buffer; incomplete
  180. return -1
  181. if (j + 4) > n:
  182. # end of buffer; incomplete
  183. return -1
  184. if rawdata[j:j+4] == "<!--":
  185. j = self.parse_comment(j, report=0)
  186. if j < 0:
  187. return j
  188. continue
  189. name, j = self._scan_name(j + 2, declstartpos)
  190. if j == -1:
  191. return -1
  192. if name not in {"attlist", "element", "entity", "notation"}:
  193. self.updatepos(declstartpos, j + 2)
  194. self.error(
  195. "unknown declaration %r in internal subset" % name)
  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. self.error("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. self.error("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. self.error("expected name token at %r"
  368. % rawdata[declstartpos:declstartpos+20])
  369. # To be overridden -- handlers for unknown objects
  370. def unknown_decl(self, data):
  371. pass