diff --git a/nbconvert/filters/strings.py b/nbconvert/filters/strings.py
index 890aa3239..6c980fce3 100644
--- a/nbconvert/filters/strings.py
+++ b/nbconvert/filters/strings.py
@@ -39,6 +39,8 @@
"text_base64",
]
+from nbconvert.filters.svg_constants import ALLOWED_SVG_ATTRIBUTES, ALLOWED_SVG_TAGS
+
def wrap_text(text, width=100):
"""
@@ -85,9 +87,10 @@ def clean_html(element):
element = str(element)
return bleach.clean(
element,
- tags=[*bleach.ALLOWED_TAGS, "div", "pre", "code", "span"],
+ tags=[*bleach.ALLOWED_TAGS, *ALLOWED_SVG_TAGS, "div", "pre", "code", "span"],
attributes={
**bleach.ALLOWED_ATTRIBUTES,
+ **{svg_tag: ALLOWED_SVG_ATTRIBUTES for svg_tag in ALLOWED_SVG_TAGS},
"*": ["class", "id"],
},
)
diff --git a/nbconvert/filters/svg_constants.py b/nbconvert/filters/svg_constants.py
new file mode 100644
index 000000000..d5ce39420
--- /dev/null
+++ b/nbconvert/filters/svg_constants.py
@@ -0,0 +1,185 @@
+# Via bleach/_vendor/html5lib/filters/sanitizer.py;
+# we don't want to import it because it would raise a deprecation warning.
+
+ALLOWED_SVG_TAGS = {
+ "a",
+ "animate",
+ "animateColor",
+ "animateMotion",
+ "animateTransform",
+ "circle",
+ "clipPath",
+ "defs",
+ "desc",
+ "ellipse",
+ "font-face",
+ "font-face-name",
+ "font-face-src",
+ "g",
+ "glyph",
+ "hkern",
+ "line",
+ "linearGradient",
+ "marker",
+ "metadata",
+ "missing-glyph",
+ "mpath",
+ "path",
+ "polygon",
+ "polyline",
+ "radialGradient",
+ "rect",
+ "set",
+ "stop",
+ "svg",
+ "switch",
+ "text",
+ "title",
+ "tspan",
+ "use",
+}
+ALLOWED_SVG_ATTRIBUTES = {
+ # SVG attributes
+ "accent-height",
+ "accumulate",
+ "additive",
+ "alphabetic",
+ "arabic-form",
+ "ascent",
+ "attributeName",
+ "attributeType",
+ "baseProfile",
+ "bbox",
+ "begin",
+ "by",
+ "calcMode",
+ "cap-height",
+ "class",
+ "clip-path",
+ "color",
+ "color-rendering",
+ "content",
+ "cx",
+ "cy",
+ "d",
+ "descent",
+ "display",
+ "dur",
+ "dx",
+ "dy",
+ "end",
+ "fill",
+ "fill-opacity",
+ "fill-rule",
+ "font-family",
+ "font-size",
+ "font-stretch",
+ "font-style",
+ "font-variant",
+ "font-weight",
+ "from",
+ "fx",
+ "fy",
+ "g1",
+ "g2",
+ "glyph-name",
+ "gradientUnits",
+ "hanging",
+ "height",
+ "horiz-adv-x",
+ "horiz-origin-x",
+ "id",
+ "ideographic",
+ "k",
+ "keyPoints",
+ "keySplines",
+ "keyTimes",
+ "lang",
+ "marker-end",
+ "marker-mid",
+ "marker-start",
+ "markerHeight",
+ "markerUnits",
+ "markerWidth",
+ "mathematical",
+ "max",
+ "min",
+ "name",
+ "offset",
+ "opacity",
+ "orient",
+ "origin",
+ "overline-position",
+ "overline-thickness",
+ "panose-1",
+ "path",
+ "pathLength",
+ "points",
+ "preserveAspectRatio",
+ "r",
+ "refX",
+ "refY",
+ "repeatCount",
+ "repeatDur",
+ "requiredExtensions",
+ "requiredFeatures",
+ "restart",
+ "rotate",
+ "rx",
+ "ry",
+ "slope",
+ "stemh",
+ "stemv",
+ "stop-color",
+ "stop-opacity",
+ "strikethrough-position",
+ "strikethrough-thickness",
+ "stroke",
+ "stroke-dasharray",
+ "stroke-dashoffset",
+ "stroke-linecap",
+ "stroke-linejoin",
+ "stroke-miterlimit",
+ "stroke-opacity",
+ "stroke-width",
+ "systemLanguage",
+ "target",
+ "text-anchor",
+ "to",
+ "transform",
+ "type",
+ "u1",
+ "u2",
+ "underline-position",
+ "underline-thickness",
+ "unicode",
+ "unicode-range",
+ "units-per-em",
+ "values",
+ "version",
+ "viewBox",
+ "visibility",
+ "width",
+ "widths",
+ "x",
+ "x-height",
+ "x1",
+ "x2",
+ "xlink:actuate",
+ "xlink:arcrole",
+ "xlink:href",
+ "xlink:href",
+ "xlink:role",
+ "xlink:show",
+ "xlink:show",
+ "xlink:title",
+ "xlink:type",
+ "xlink:type",
+ "xml:base",
+ "xml:lang",
+ "xml:space",
+ "y",
+ "y1",
+ "y2",
+ "zoomAndPan",
+}
diff --git a/nbconvert/tests/files/issue1849_svg.ipynb b/nbconvert/tests/files/issue1849_svg.ipynb
new file mode 100644
index 000000000..3cbac9d88
--- /dev/null
+++ b/nbconvert/tests/files/issue1849_svg.ipynb
@@ -0,0 +1,500 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "bec574e4-517a-4d87-b4db-f2368491afdc",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import matplotlib.pyplot as plt\n",
+ "import matplotlib_inline"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "fbf233aa-84a5-48b4-bb8c-e05bd2e55391",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "matplotlib_inline.backend_inline.set_matplotlib_formats('svg')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "62406ac3-51eb-4810-8b7b-e031ff9abd77",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/svg+xml": [
+ "\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "plt.plot([1, 2]);"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.10.6"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/nbconvert/tests/test_nbconvertapp.py b/nbconvert/tests/test_nbconvertapp.py
index 0d2cd4826..a097c7ead 100644
--- a/nbconvert/tests/test_nbconvertapp.py
+++ b/nbconvert/tests/test_nbconvertapp.py
@@ -595,6 +595,16 @@ def test_embedding_images_htmlexporter(self):
assert "src='./containerized_deployments.jpeg'" not in text
assert text.count("data:image/jpeg;base64") == 3
+ def test_embedded_svg_remains(self):
+ """Check that the HTMLExporter doesn't scrub SVG"""
+
+ with self.create_temp_cwd(["issue1849_svg.ipynb"]):
+ self.nbconvert("issue1849_svg --log-level 0 --to html")
+ assert os.path.isfile("issue1849_svg.html")
+ with open("issue1849_svg.html", encoding="utf8") as f:
+ text = f.read()
+ assert '' in text # Must not be escaped
+
def test_execute_widgets_from_nbconvert(self):
"""Check jupyter widgets render"""
notebookName = "Unexecuted_widget"