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
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
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# --------------------------------------------------------------------
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:
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"""
46from __future__ import annotations
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
54__all__ = ['to_html_string', 'to_xhtml_string']
56RE_AMP = re.compile(r'&(?!(?:\#[0-9]+|\#x[0-9a-f]+|[0-9a-z]+);)', re.I)
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}
80def _raise_serialization_error(text: str) -> NoReturn: # pragma: no cover
81 raise TypeError(
82 "cannot serialize {!r} (type {})".format(text, type(text).__name__)
83 )
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('&', text)
95 if "<" in text:
96 text = text.replace("<", "<")
97 if ">" in text:
98 text = text.replace(">", ">")
99 return text
100 except (TypeError, AttributeError): # pragma: no cover
101 _raise_serialization_error(text)
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('&', text)
110 if "<" in text:
111 text = text.replace("<", "<")
112 if ">" in text:
113 text = text.replace(">", ">")
114 if "\"" in text:
115 text = text.replace("\"", """)
116 if "\n" in text:
117 text = text.replace("\n", " ")
118 return text
119 except (TypeError, AttributeError): # pragma: no cover
120 _raise_serialization_error(text)
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('&', text)
129 if "<" in text:
130 text = text.replace("<", "<")
131 if ">" in text:
132 text = text.replace(">", ">")
133 if "\"" in text:
134 text = text.replace("\"", """)
135 return text
136 except (TypeError, AttributeError): # pragma: no cover
137 _raise_serialization_error(text)
140def _serialize_html(write: Callable[[str], None], elem: Element, format: Literal["html", "xhtml"]) -> None:
141 stack: deque[Element | str] = deque([elem])
143 count = 0
144 while stack:
145 count += 1
146 el = stack.popleft()
148 if isinstance(el, str):
149 write(el)
150 continue
152 tag = el.tag
153 text = el.text
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))
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)
228# --------------------------------------------------------------------
229# public functions
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")
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")