From 9809c539390d31579c31a8cd6ac61c300e935bfa Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 26 Oct 2022 13:33:30 +0300 Subject: [PATCH] clean_html: allow SVG tags and SVG attributes Fixes #1849 Refs #1854 --- nbconvert/filters/strings.py | 5 +- nbconvert/filters/svg_constants.py | 185 ++++++++ nbconvert/tests/files/issue1849_svg.ipynb | 500 ++++++++++++++++++++++ nbconvert/tests/test_nbconvertapp.py | 10 + 4 files changed, 699 insertions(+), 1 deletion(-) create mode 100644 nbconvert/filters/svg_constants.py create mode 100644 nbconvert/tests/files/issue1849_svg.ipynb 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", + " \n", + " \n", + " \n", + " \n", + " 2022-08-23T17:20:50.390281\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.5.3, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \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"