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

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

110 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 

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

60 raise TypeError( 

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

62 ) 

63 

64 

65def _escape_cdata(text) -> str: 

66 # escape character data 

67 try: 

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

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

70 # the most common case in most applications. 

71 if "&" in text: 

72 # Only replace & when not part of an entity 

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

74 if "<" in text: 

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

76 if ">" in text: 

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

78 return text 

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

80 _raise_serialization_error(text) 

81 

82 

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

84 # escape attribute value 

85 try: 

86 if "&" in text: 

87 # Only replace & when not part of an entity 

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

89 if "<" in text: 

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

91 if ">" in text: 

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

93 if "\"" in text: 

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

95 if "\n" in text: 

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

97 return text 

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

99 _raise_serialization_error(text) 

100 

101 

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

103 # escape attribute value 

104 try: 

105 if "&" in text: 

106 # Only replace & when not part of an entity 

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

108 if "<" in text: 

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

110 if ">" in text: 

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

112 if "\"" in text: 

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

114 return text 

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

116 _raise_serialization_error(text) 

117 

118 

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

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

121 

122 count = 0 

123 while stack: 

124 count += 1 

125 el = stack.popleft() 

126 

127 if isinstance(el, str): 

128 write(el) 

129 continue 

130 

131 tag = el.tag 

132 text = el.text 

133 

134 if tag is Comment: 

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

136 elif tag is ProcessingInstruction: 

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

138 elif tag is None: 

139 if text: 

140 write(_escape_cdata(text)) 

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

142 if el.tail: 

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

144 stack.extendleft(reversed(el)) 

145 continue 

146 else: 

147 namespace_uri = None 

148 if isinstance(tag, QName): 

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

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

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

152 else: 

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

154 write(f"<{tag}") 

155 items = el.items() 

156 if items: 

157 items = sorted(items) # lexical order 

158 for k, v in items: 

159 if isinstance(k, QName): 

160 # Assume a text only `QName` 

161 k = k.text 

162 if isinstance(v, QName): 

163 # Assume a text only `QName` 

164 v = v.text 

165 else: 

166 v = _escape_attrib_html(v) 

167 if k == v and format == 'html': 

168 # handle boolean attributes 

169 write(f" {v}") 

170 else: 

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

172 if namespace_uri: 

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

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

175 write(" />") 

176 else: 

177 write(">") 

178 if text: 

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

180 write(text) 

181 else: 

182 write(_escape_cdata(text)) 

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

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

185 if el.tail: 

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

187 if tag.lower() not in HTML_EMPTY: 

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

189 stack.extendleft(reversed(el)) 

190 continue 

191 if el.tail: 

192 write(_escape_cdata(el.tail)) 

193 

194 

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

196 assert root is not None 

197 data: list[str] = [] 

198 write = data.append 

199 _serialize_html(write, root, format) 

200 return "".join(data) 

201 

202 

203# -------------------------------------------------------------------- 

204# public functions 

205 

206 

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

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

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

210 

211 

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

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

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