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
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)
59def _raise_serialization_error(text: str) -> NoReturn: # pragma: no cover
60 raise TypeError(
61 "cannot serialize {!r} (type {})".format(text, type(text).__name__)
62 )
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('&', text)
74 if "<" in text:
75 text = text.replace("<", "<")
76 if ">" in text:
77 text = text.replace(">", ">")
78 return text
79 except (TypeError, AttributeError): # pragma: no cover
80 _raise_serialization_error(text)
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('&', text)
89 if "<" in text:
90 text = text.replace("<", "<")
91 if ">" in text:
92 text = text.replace(">", ">")
93 if "\"" in text:
94 text = text.replace("\"", """)
95 if "\n" in text:
96 text = text.replace("\n", " ")
97 return text
98 except (TypeError, AttributeError): # pragma: no cover
99 _raise_serialization_error(text)
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('&', text)
108 if "<" in text:
109 text = text.replace("<", "<")
110 if ">" in text:
111 text = text.replace(">", ">")
112 if "\"" in text:
113 text = text.replace("\"", """)
114 return text
115 except (TypeError, AttributeError): # pragma: no cover
116 _raise_serialization_error(text)
119def _serialize_html(write: Callable[[str], None], elem: Element, format: Literal["html", "xhtml"]) -> None:
120 stack: deque[Element | str] = deque([elem])
122 count = 0
123 while stack:
124 count += 1
125 el = stack.popleft()
127 if isinstance(el, str):
128 write(el)
129 continue
131 tag = el.tag
132 text = el.text
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))
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)
203# --------------------------------------------------------------------
204# public functions
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")
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")