Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/markdown/serializers.py: 73%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

113 statements  

1# Add x/html serialization to `Elementree` 

2# Taken from ElementTree 1.3 preview with slight modifications 

3# 

4# Copyright (c) 1999-2007 by Fredrik Lundh. All rights reserved. 

5# 

6# fredrik@pythonware.com 

7# https://www.pythonware.com/ 

8# 

9# -------------------------------------------------------------------- 

10# The ElementTree toolkit is 

11# 

12# Copyright (c) 1999-2007 by Fredrik Lundh 

13# 

14# By obtaining, using, and/or copying this software and/or its 

15# associated documentation, you agree that you have read, understood, 

16# and will comply with the following terms and conditions: 

17# 

18# Permission to use, copy, modify, and distribute this software and 

19# its associated documentation for any purpose and without fee is 

20# hereby granted, provided that the above copyright notice appears in 

21# all copies, and that both that copyright notice and this permission 

22# notice appear in supporting documentation, and that the name of 

23# Secret Labs AB or the author not be used in advertising or publicity 

24# pertaining to distribution of the software without specific, written 

25# prior permission. 

26# 

27# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD 

28# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT- 

29# ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR 

30# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY 

31# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, 

32# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS 

33# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE 

34# OF THIS SOFTWARE. 

35# -------------------------------------------------------------------- 

36 

37""" 

38Python-Markdown provides two serializers which render [`ElementTree.Element`][xml.etree.ElementTree.Element] 

39objects to a string of HTML. Both functions wrap the same underlying code with only a few minor 

40differences as outlined below: 

41 

421. Empty (self-closing) tags are rendered as `<tag>` for HTML and as `<tag />` for XHTML. 

432. Boolean attributes are rendered as `attrname` for HTML and as `attrname="attrname"` for XHTML. 

44""" 

45 

46from __future__ import annotations 

47 

48from xml.etree.ElementTree import ProcessingInstruction 

49from xml.etree.ElementTree import Comment, ElementTree, Element, QName, HTML_EMPTY 

50import re 

51from collections import deque 

52from typing import Callable, Literal, NoReturn 

53 

54__all__ = ['to_html_string', 'to_xhtml_string'] 

55 

56RE_AMP = re.compile(r'&(?!(?:\#[0-9]+|\#x[0-9a-f]+|[0-9a-z]+);)', re.I) 

57 

58# Known boolean attributes, whose presence alone carries the meaning and 

59# which may therefore be rendered in the minimized (valueless) form. The 

60# list of recognized attributes differs per output format: `xhtml` only 

61# recognizes the attributes declared boolean by the XHTML 1.0 spec, while 

62# `html` recognizes the boolean attributes of the HTML spec. Note that 

63# `download` is deliberately absent from both lists: it is only boolean 

64# when no value is assigned and a value changes its meaning. 

65BOOLEAN_ATTRIBUTES: dict[str, frozenset[str]] = { 

66 'html': frozenset([ 

67 'allowfullscreen', 'async', 'autofocus', 'autoplay', 'checked', 

68 'controls', 'default', 'defer', 'disabled', 'formnovalidate', 

69 'hidden', 'inert', 'ismap', 'itemscope', 'loop', 'multiple', 'muted', 

70 'nomodule', 'novalidate', 'open', 'playsinline', 'readonly', 

71 'required', 'reversed', 'selected', 

72 ]), 

73 'xhtml': frozenset([ 

74 'checked', 'compact', 'declare', 'defer', 'disabled', 'ismap', 

75 'multiple', 'noresize', 'noshade', 'nowrap', 'readonly', 'selected', 

76 ]), 

77} 

78 

79 

80def _raise_serialization_error(text: str) -> NoReturn: # pragma: no cover 

81 raise TypeError( 

82 "cannot serialize {!r} (type {})".format(text, type(text).__name__) 

83 ) 

84 

85 

86def _escape_cdata(text) -> str: 

87 # escape character data 

88 try: 

89 # it's worth avoiding do-nothing calls for strings that are 

90 # shorter than 500 character, or so. assume that's, by far, 

91 # the most common case in most applications. 

92 if "&" in text: 

93 # Only replace & when not part of an entity 

94 text = RE_AMP.sub('&amp;', text) 

95 if "<" in text: 

96 text = text.replace("<", "&lt;") 

97 if ">" in text: 

98 text = text.replace(">", "&gt;") 

99 return text 

100 except (TypeError, AttributeError): # pragma: no cover 

101 _raise_serialization_error(text) 

102 

103 

104def _escape_attrib(text: str) -> str: 

105 # escape attribute value 

106 try: 

107 if "&" in text: 

108 # Only replace & when not part of an entity 

109 text = RE_AMP.sub('&amp;', text) 

110 if "<" in text: 

111 text = text.replace("<", "&lt;") 

112 if ">" in text: 

113 text = text.replace(">", "&gt;") 

114 if "\"" in text: 

115 text = text.replace("\"", "&quot;") 

116 if "\n" in text: 

117 text = text.replace("\n", "&#10;") 

118 return text 

119 except (TypeError, AttributeError): # pragma: no cover 

120 _raise_serialization_error(text) 

121 

122 

123def _escape_attrib_html(text: str) -> str: 

124 # escape attribute value 

125 try: 

126 if "&" in text: 

127 # Only replace & when not part of an entity 

128 text = RE_AMP.sub('&amp;', text) 

129 if "<" in text: 

130 text = text.replace("<", "&lt;") 

131 if ">" in text: 

132 text = text.replace(">", "&gt;") 

133 if "\"" in text: 

134 text = text.replace("\"", "&quot;") 

135 return text 

136 except (TypeError, AttributeError): # pragma: no cover 

137 _raise_serialization_error(text) 

138 

139 

140def _serialize_html(write: Callable[[str], None], elem: Element, format: Literal["html", "xhtml"]) -> None: 

141 stack: deque[Element | str] = deque([elem]) 

142 

143 count = 0 

144 while stack: 

145 count += 1 

146 el = stack.popleft() 

147 

148 if isinstance(el, str): 

149 write(el) 

150 continue 

151 

152 tag = el.tag 

153 text = el.text 

154 

155 if tag is Comment: 

156 write(f"<!--{_escape_cdata(text)}-->") 

157 elif tag is ProcessingInstruction: 

158 write(f"<?{_escape_cdata(text)}?>") 

159 elif tag is None: 

160 if text: 

161 write(_escape_cdata(text)) 

162 # Add the children in reverse order so we process them in the right order. 

163 if el.tail: 

164 stack.appendleft(_escape_cdata(el.tail)) 

165 stack.extendleft(reversed(el)) 

166 continue 

167 else: 

168 namespace_uri = None 

169 if isinstance(tag, QName): 

170 # `QNAME` objects store their data as a string: `{uri}tag` 

171 if tag.text[:1] == "{": 

172 namespace_uri, tag = tag.text[1:].split("}", 1) 

173 else: 

174 raise ValueError('QName objects must define a tag.') 

175 write(f"<{tag}") 

176 items = el.items() 

177 if items: 

178 items = sorted(items) # lexical order 

179 for k, v in items: 

180 if isinstance(k, QName): 

181 # Assume a text only `QName` 

182 k = k.text 

183 if isinstance(v, QName): 

184 # Assume a text only `QName` 

185 v = v.text 

186 if v is None: 

187 # A valueless attribute is preserved exactly as it 

188 # was provided: render the bare attribute name. 

189 write(f" {k}") 

190 else: 

191 v = _escape_attrib_html(v) 

192 if k == v and format == 'html' and k in BOOLEAN_ATTRIBUTES['html']: 

193 # handle boolean attributes 

194 write(f" {v}") 

195 else: 

196 write(f' {k}="{v}"') 

197 if namespace_uri: 

198 write(f' xmlns="{_escape_attrib(namespace_uri)}"') 

199 if format == "xhtml" and tag.lower() in HTML_EMPTY: 

200 write(" />") 

201 else: 

202 write(">") 

203 if text: 

204 if tag.lower() in ["script", "style"]: 

205 write(text) 

206 else: 

207 write(_escape_cdata(text)) 

208 # Add the tail, end tag, and then the children in reverse order. 

209 # Since we pop from the left, this will ensure results are ordered correctly. 

210 if el.tail: 

211 stack.appendleft(_escape_cdata(el.tail)) 

212 if tag.lower() not in HTML_EMPTY: 

213 stack.appendleft(f"</{tag}>") 

214 stack.extendleft(reversed(el)) 

215 continue 

216 if el.tail: 

217 write(_escape_cdata(el.tail)) 

218 

219 

220def _write_html(root: Element, format: Literal["html", "xhtml"] = "html") -> str: 

221 assert root is not None 

222 data: list[str] = [] 

223 write = data.append 

224 _serialize_html(write, root, format) 

225 return "".join(data) 

226 

227 

228# -------------------------------------------------------------------- 

229# public functions 

230 

231 

232def to_html_string(element: Element) -> str: 

233 """ Serialize element and its children to a string of HTML5. """ 

234 return _write_html(ElementTree(element).getroot(), format="html") 

235 

236 

237def to_xhtml_string(element: Element) -> str: 

238 """ Serialize element and its children to a string of XHTML. """ 

239 return _write_html(ElementTree(element).getroot(), format="xhtml")