Repository: jupyter/nbconvert
Pull request #2021 (2023-07-10 14:42:24)

[Accessibility] some accessibility improvements
This PR improves the accessibility of the produced HTML file, using the `HTMLExporter`:
- adds a main landmark for the Notebook content
- adds a lang attribute to the `html` tag, configurable with a trait
- adds missing alternative text on images produced by matplotlib
- adds `tabindex="0"` to the inputs and outputs, in order to be able to navigate through the Notebook using the tab key

mgeier: I'm not a user of a screenreader, so I might be missing something, but how is `alt="Image"` helping anyone? Doesn't a screenreader already tell you it's an image? Wouldn't it say "image" twice? Assuming the screenreader is even used by an english-speaking user ...

I don't know a solution here, but I would like to point out previous discussions: #1993, https://github.com/matplotlib/matplotlib/pull/21328
brichet: > I'm not a user of a screenreader, so I might be missing something, but how is `alt="Image"` helping anyone?

Thanks for pointing out some discussions.

I agree that it's not really useful as such.
Nevertheless, it seems that the screenreader needs it, at least the Deque University Axe Devtools complains about it. But for informative image, the alternative text should be informative: https://dequeuniversity.com/rules/axe/4.4/image-alt, which is not the case here.

I don't think we can even tell that it's an image from matplotlib when building HTML, so I don't know neither how we can have any information about the content.
The best solution is to have if fixed from matplotlib.

Patch:
diff --git a/nbconvert/exporters/html.py b/nbconvert/exporters/html.py
index 30c10b7f..055e6991 100644
--- a/nbconvert/exporters/html.py
+++ b/nbconvert/exporters/html.py
@@ -12,8 +12,9 @@ from typing import Any, Dict, Optional, Tuple
 
 import jinja2
 import markupsafe
+from bs4 import BeautifulSoup
 from jupyter_core.paths import jupyter_path
-from traitlets import Bool, Unicode, default
+from traitlets import Bool, Unicode, default, validate
 from traitlets.config import Config
 
 if tuple(int(x) for x in jinja2.__version__.split(".")[:3]) < (3, 0, 0):
@@ -27,6 +28,7 @@ from nbformat import NotebookNode
 from nbconvert.filters.highlight import Highlight2HTML
 from nbconvert.filters.markdown_mistune import IPythonRenderer, MarkdownWithMath
 from nbconvert.filters.widgetsdatatypefilter import WidgetsDataTypeFilter
+from nbconvert.utils.iso639_1 import iso639_1
 
 from .templateexporter import TemplateExporter
 
@@ -202,6 +204,20 @@ class HTMLExporter(TemplateExporter):
             c = c2
         return c
 
+    language_code = Unicode(
+        "en", help="Language code of the content, should be one of the ISO639-1"
+    ).tag(config=True)
+
+    @validate("language_code")
+    def _valid_language_code(self, proposal):
+        if self.language_code not in iso639_1:
+            self.log.warn(
+                f'"{self.language_code}" is not an ISO 639-1 language code. '
+                'It has been replaced by the default value "en".'
+            )
+            return proposal["trait"].default_value
+        return proposal["value"]
+
     @contextfilter
     def markdown2html(self, context, source):
         """Markdown to HTML filter respecting the anchor_link_text setting"""
@@ -240,7 +256,18 @@ class HTMLExporter(TemplateExporter):
 
         self.register_filter("highlight_code", highlight_code)
         self.register_filter("filter_data_type", filter_data_type)
-        return super().from_notebook_node(nb, resources, **kw)
+        html, resources = super().from_notebook_node(nb, resources, **kw)
+        soup = BeautifulSoup(html, features="html.parser")
+        # Add image's alternative text
+        for elem in soup.select("img:not([alt])"):
+            elem.attrs["alt"] = "Image"
+        # Set input and output focusable
+        for elem in soup.select(".jp-Notebook div.jp-Cell-inputWrapper"):
+            elem.attrs["tabindex"] = "0"
+        for elem in soup.select(".jp-Notebook div.jp-OutputArea-output"):
+            elem.attrs["tabindex"] = "0"
+
+        return str(soup), resources
 
     def _init_resources(self, resources):  # noqa
         def resources_include_css(name):
@@ -318,4 +345,5 @@ class HTMLExporter(TemplateExporter):
         resources["widget_renderer_url"] = self.widget_renderer_url
         resources["html_manager_semver_range"] = self.html_manager_semver_range
         resources["should_sanitize_html"] = self.sanitize_html
+        resources["language_code"] = self.language_code
         return resources
diff --git a/nbconvert/filters/strings.py b/nbconvert/filters/strings.py
index 03f06815..c84de083 100644
--- a/nbconvert/filters/strings.py
+++ b/nbconvert/filters/strings.py
@@ -41,8 +41,6 @@ __all__ = [
     "text_base64",
 ]
 
-from nbconvert.filters.svg_constants import ALLOWED_SVG_ATTRIBUTES, ALLOWED_SVG_TAGS
-
 
 def wrap_text(text, width=100):
     """
@@ -91,11 +89,9 @@ def clean_html(element):
         kwargs['css_sanitizer'] = css_sanitizer
     return bleach.clean(
         element,
-        tags=[*bleach.ALLOWED_TAGS, *ALLOWED_SVG_TAGS, "div", "pre", "code", "span"],
-        strip_comments=False,
+        tags=[*bleach.ALLOWED_TAGS, "div", "pre", "code", "span"],
         attributes={
             **bleach.ALLOWED_ATTRIBUTES,
-            **{svg_tag: list(ALLOWED_SVG_ATTRIBUTES) for svg_tag in ALLOWED_SVG_TAGS},
             "*": ["class", "id"],
         },
         **kwargs,
diff --git a/nbconvert/filters/svg_constants.py b/nbconvert/filters/svg_constants.py
deleted file mode 100644
index 13029bf9..00000000
--- a/nbconvert/filters/svg_constants.py
+++ /dev/null
@@ -1,190 +0,0 @@
-"""SVG constants for sanitization."""
-# These are lifted from html5lib's sanitizer module, which is deprecated.
-#
-# Quoth the migration guide (https://github.com/mozilla/bleach/blob/main/docs/migrating.rst#different-allow-lists):
-#
-# > If you want to stick to the html5lib sanitizer's allow lists, get them from the sanitizer code.
-# > It's probably best to copy them as static lists.
-
-# TODO: Bleach currently drops attribute namespaces; cleaning SVGs will probably require more elbow grease.
-#       See https://github.com/mozilla/bleach/issues/362
-
-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 = {
-    "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",
-    "style",
-    "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:role",
-    "xlink:show",
-    "xlink:title",
-    "xlink:type",
-    "xml:base",
-    "xml:lang",
-    "xml:space",
-    "y",
-    "y1",
-    "y2",
-    "zoomAndPan",
-}
diff --git a/nbconvert/utils/iso639_1.py b/nbconvert/utils/iso639_1.py
new file mode 100644
index 00000000..ce5c2c66
--- /dev/null
+++ b/nbconvert/utils/iso639_1.py
@@ -0,0 +1,191 @@
+""" List of ISO639-1 language code"""
+
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+iso639_1 = [
+    "aa",
+    "ab",
+    "ae",
+    "af",
+    "ak",
+    "am",
+    "an",
+    "ar",
+    "as",
+    "av",
+    "ay",
+    "az",
+    "ba",
+    "be",
+    "bg",
+    "bh",
+    "bi",
+    "bm",
+    "bn",
+    "bo",
+    "br",
+    "bs",
+    "ca",
+    "ce",
+    "ch",
+    "co",
+    "cr",
+    "cs",
+    "cu",
+    "cv",
+    "cy",
+    "da",
+    "de",
+    "dv",
+    "dz",
+    "ee",
+    "el",
+    "en",
+    "eo",
+    "es",
+    "et",
+    "eu",
+    "fa",
+    "ff",
+    "fi",
+    "fj",
+    "fo",
+    "fr",
+    "fy",
+    "ga",
+    "gd",
+    "gl",
+    "gn",
+    "gu",
+    "gv",
+    "ha",
+    "he",
+    "hi",
+    "ho",
+    "hr",
+    "ht",
+    "hu",
+    "hy",
+    "hz",
+    "ia",
+    "id",
+    "ie",
+    "ig",
+    "ii",
+    "ik",
+    "io",
+    "is",
+    "it",
+    "iu",
+    "ja",
+    "jv",
+    "ka",
+    "kg",
+    "ki",
+    "kj",
+    "kk",
+    "kl",
+    "km",
+    "kn",
+    "ko",
+    "kr",
+    "ks",
+    "ku",
+    "kv",
+    "kw",
+    "ky",
+    "la",
+    "lb",
+    "lg",
+    "li",
+    "ln",
+    "lo",
+    "lt",
+    "lu",
+    "lv",
+    "mg",
+    "mh",
+    "mi",
+    "mk",
+    "ml",
+    "mn",
+    "mr",
+    "ms",
+    "mt",
+    "my",
+    "na",
+    "nb",
+    "nd",
+    "ne",
+    "ng",
+    "nl",
+    "nn",
+    "no",
+    "nr",
+    "nv",
+    "ny",
+    "oc",
+    "oj",
+    "om",
+    "or",
+    "os",
+    "pa",
+    "pi",
+    "pl",
+    "ps",
+    "pt",
+    "qu",
+    "rm",
+    "rn",
+    "ro",
+    "ru",
+    "rw",
+    "sa",
+    "sc",
+    "sd",
+    "se",
+    "sg",
+    "si",
+    "sk",
+    "sl",
+    "sm",
+    "sn",
+    "so",
+    "sq",
+    "sr",
+    "ss",
+    "st",
+    "su",
+    "sv",
+    "sw",
+    "ta",
+    "te",
+    "tg",
+    "th",
+    "ti",
+    "tk",
+    "tl",
+    "tn",
+    "to",
+    "tr",
+    "ts",
+    "tt",
+    "tw",
+    "ty",
+    "ug",
+    "uk",
+    "ur",
+    "uz",
+    "ve",
+    "vi",
+    "vo",
+    "wa",
+    "wo",
+    "xh",
+    "yi",
+    "yo",
+    "za",
+    "zh",
+    "zu",
+]
diff --git a/share/templates/classic/base.html.j2 b/share/templates/classic/base.html.j2
index 640067bf..3a0f75f5 100644
--- a/share/templates/classic/base.html.j2
+++ b/share/templates/classic/base.html.j2
@@ -132,7 +132,7 @@ unknown type  {{ cell.type }}
 {%- if output.svg_filename %}
 <img src="{{ output.svg_filename | posix_path | escape_html }}">
 {%- else %}
-{{ output.data['image/svg+xml'].encode("utf-8") | clean_html }}
+<img src="data:image/svg+xml;base64,{{ output.data['image/svg+xml'] | text_base64 | escape_html }}">
 {%- endif %}
 </div>
 {%- endblock data_svg %}
diff --git a/share/templates/classic/index.html.j2 b/share/templates/classic/index.html.j2
index 87a8578a..30cd5026 100644
--- a/share/templates/classic/index.html.j2
+++ b/share/templates/classic/index.html.j2
@@ -4,7 +4,7 @@
 
 {%- block header -%}
 <!DOCTYPE html>
-<html>
+<html lang="{{ resources.language_code }}">
 <head>
 {%- block html_head -%}
 <meta charset="utf-8" />
@@ -92,13 +92,15 @@ div#notebook-container{
 
 {% block body_header %}
 <body>
-  <div tabindex="-1" id="notebook" class="border-box-sizing">
-    <div class="container" id="notebook-container">
+  <main>
+    <div tabindex="-1" id="notebook" class="border-box-sizing">
+      <div class="container" id="notebook-container">
 {% endblock body_header %}
 
 {% block body_footer %}
+      </div>
     </div>
-  </div>
+  </main>
 </body>
 {% endblock body_footer %}
 
diff --git a/share/templates/lab/base.html.j2 b/share/templates/lab/base.html.j2
index 9f376848..255b1a66 100644
--- a/share/templates/lab/base.html.j2
+++ b/share/templates/lab/base.html.j2
@@ -164,7 +164,7 @@ unknown type  {{ cell.type }}
 {%- if output.svg_filename %}
 <img src="{{ output.svg_filename | posix_path | escape_html }}">
 {%- else %}
-{{ output.data['image/svg+xml'].encode("utf-8") | clean_html }}
+<img src="data:image/svg+xml;base64,{{ output.data['image/svg+xml'] | text_base64 | escape_html }}">
 {%- endif %}
 </div>
 {%- endblock data_svg %}
diff --git a/share/templates/lab/index.html.j2 b/share/templates/lab/index.html.j2
index 7549a453..ad184ca7 100644
--- a/share/templates/lab/index.html.j2
+++ b/share/templates/lab/index.html.j2
@@ -5,7 +5,7 @@
 
 {%- block header -%}
 <!DOCTYPE html>
-<html>
+<html lang="{{ resources.language_code }}">
 <head>
 {%- block html_head -%}
 <meta charset="utf-8" />
@@ -125,9 +125,11 @@ a.anchor-link {
 {% else %}
 <body class="jp-Notebook" data-jp-theme-light="true" data-jp-theme-name="JupyterLab Light">
 {% endif %}
+<main>
 {%- endblock body_header -%}
 
 {% block body_footer %}
+</main>
 </body>
 {% endblock body_footer %}
 
diff --git a/share/templates/reveal/index.html.j2 b/share/templates/reveal/index.html.j2
index 84cb6528..26fed48c 100644
--- a/share/templates/reveal/index.html.j2
+++ b/share/templates/reveal/index.html.j2
@@ -10,7 +10,7 @@
 
 {%- block header -%}
 <!DOCTYPE html>
-<html>
+<html lang="{{ resources.language_code }}">
 <head>
 
 {%- block html_head -%}
@@ -118,6 +118,7 @@ a.anchor-link {
 {% else %}
 <body class="jp-Notebook" data-jp-theme-light="true" data-jp-theme-name="JupyterLab Light">
 {% endif %}
+<main>
 <div class="reveal">
 <div class="slides">
 {%- endblock body_header -%}
@@ -125,6 +126,7 @@ a.anchor-link {
 {% block body_footer %}
 </div>
 </div>
+</main>
 </body>
 {% endblock body_footer %}
 


Test patch:
diff --git a/nbconvert/exporters/tests/test_html.py b/nbconvert/exporters/tests/test_html.py
index 6d5db7c3..04c2a74f 100644
--- a/nbconvert/exporters/tests/test_html.py
+++ b/nbconvert/exporters/tests/test_html.py
@@ -76,12 +76,12 @@ class TestHTMLExporter(ExportersTestsBase):
         (output, resources) = HTMLExporter(template_name="classic").from_filename(
             self._get_notebook(nb_name="pngmetadata.ipynb")
         )
-        check_for_png = re.compile(r'<img src="[^"]*?"([^>]*?)>')
+        check_for_png = re.compile(r'<img alt="Image"([^>]*?)>')
         result = check_for_png.search(output)
         assert result
         attr_string = result.group(1)
-        assert "width" in attr_string
-        assert "height" in attr_string
+        assert "width=" in attr_string
+        assert "height=" in attr_string
 
     def test_javascript_output(self):
         nb = v4.new_notebook(
@@ -103,13 +103,12 @@ class TestHTMLExporter(ExportersTestsBase):
         (output, resources) = HTMLExporter(template_name="classic").from_file(
             self._get_notebook(nb_name="attachment.ipynb")
         )
-        check_for_png = re.compile(r'<img src="[^"]*?"([^>]*?)>')
+        check_for_png = re.compile(r'<img alt="image.png" src="([^"]*?)"/>')
         result = check_for_png.search(output)
         assert result
-        self.assertTrue(result.group(0).strip().startswith('<img src="data:image/png;base64,iVBOR'))
-        self.assertTrue(result.group(1).strip().startswith('alt="image.png"'))
+        self.assertTrue(result.group(1).strip().startswith('data:image/png;base64,iVBOR'))
 
-        check_for_data = re.compile(r'<img src="(?P<url>[^"]*?)"')
+        check_for_data = re.compile(r'<img alt="image.png" src="(?P<url>[^"]*?)"')
         results = check_for_data.findall(output)
         assert results[0] != results[1], "attachments only need to be unique within a cell"
         assert "image/svg" in results[1], "second image should use svg"
diff --git a/nbconvert/tests/files/issue1849_svg.ipynb b/nbconvert/tests/files/issue1849_svg.ipynb
deleted file mode 100644
index 3cbac9d8..00000000
--- a/nbconvert/tests/files/issue1849_svg.ipynb
+++ /dev/null
@@ -1,500 +0,0 @@
-{
- "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": [
-       "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"no\"?>\n",
-       "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n",
-       "  \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n",
-       "<svg xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"394.423125pt\" height=\"297.190125pt\" viewBox=\"0 0 394.423125 297.190125\" xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\">\n",
-       " <metadata>\n",
-       "  <rdf:RDF xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:cc=\"http://creativecommons.org/ns#\" xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">\n",
-       "   <cc:Work>\n",
-       "    <dc:type rdf:resource=\"http://purl.org/dc/dcmitype/StillImage\"/>\n",
-       "    <dc:date>2022-08-23T17:20:50.390281</dc:date>\n",
-       "    <dc:format>image/svg+xml</dc:format>\n",
-       "    <dc:creator>\n",
-       "     <cc:Agent>\n",
-       "      <dc:title>Matplotlib v3.5.3, https://matplotlib.org/</dc:title>\n",
-       "     </cc:Agent>\n",
-       "    </dc:creator>\n",
-       "   </cc:Work>\n",
-       "  </rdf:RDF>\n",
-       " </metadata>\n",
-       " <defs>\n",
-       "  <style type=\"text/css\">*{stroke-linejoin: round; stroke-linecap: butt}</style>\n",
-       " </defs>\n",
-       " <g id=\"figure_1\">\n",
-       "  <g id=\"patch_1\">\n",
-       "   <path d=\"M 0 297.190125 \n",
-       "L 394.423125 297.190125 \n",
-       "L 394.423125 0 \n",
-       "L 0 0 \n",
-       "z\n",
-       "\" style=\"fill: #ffffff\"/>\n",
-       "  </g>\n",
-       "  <g id=\"axes_1\">\n",
-       "   <g id=\"patch_2\">\n",
-       "    <path d=\"M 30.103125 273.312 \n",
-       "L 387.223125 273.312 \n",
-       "L 387.223125 7.2 \n",
-       "L 30.103125 7.2 \n",
-       "z\n",
-       "\" style=\"fill: #ffffff\"/>\n",
-       "   </g>\n",
-       "   <g id=\"matplotlib.axis_1\">\n",
-       "    <g id=\"xtick_1\">\n",
-       "     <g id=\"line2d_1\">\n",
-       "      <defs>\n",
-       "       <path id=\"m361cdeea3f\" d=\"M 0 0 \n",
-       "L 0 3.5 \n",
-       "\" style=\"stroke: #000000; stroke-width: 0.8\"/>\n",
-       "      </defs>\n",
-       "      <g>\n",
-       "       <use xlink:href=\"#m361cdeea3f\" x=\"46.335852\" y=\"273.312\" style=\"stroke: #000000; stroke-width: 0.8\"/>\n",
-       "      </g>\n",
-       "     </g>\n",
-       "     <g id=\"text_1\">\n",
-       "      <!-- 0.0 -->\n",
-       "      <g transform=\"translate(38.38429 287.910437)scale(0.1 -0.1)\">\n",
-       "       <defs>\n",
-       "        <path id=\"DejaVuSans-30\" d=\"M 2034 4250 \n",
-       "Q 1547 4250 1301 3770 \n",
-       "Q 1056 3291 1056 2328 \n",
-       "Q 1056 1369 1301 889 \n",
-       "Q 1547 409 2034 409 \n",
-       "Q 2525 409 2770 889 \n",
-       "Q 3016 1369 3016 2328 \n",
-       "Q 3016 3291 2770 3770 \n",
-       "Q 2525 4250 2034 4250 \n",
-       "z\n",
-       "M 2034 4750 \n",
-       "Q 2819 4750 3233 4129 \n",
-       "Q 3647 3509 3647 2328 \n",
-       "Q 3647 1150 3233 529 \n",
-       "Q 2819 -91 2034 -91 \n",
-       "Q 1250 -91 836 529 \n",
-       "Q 422 1150 422 2328 \n",
-       "Q 422 3509 836 4129 \n",
-       "Q 1250 4750 2034 4750 \n",
-       "z\n",
-       "\" transform=\"scale(0.015625)\"/>\n",
-       "        <path id=\"DejaVuSans-2e\" d=\"M 684 794 \n",
-       "L 1344 794 \n",
-       "L 1344 0 \n",
-       "L 684 0 \n",
-       "L 684 794 \n",
-       "z\n",
-       "\" transform=\"scale(0.015625)\"/>\n",
-       "       </defs>\n",
-       "       <use xlink:href=\"#DejaVuSans-30\"/>\n",
-       "       <use xlink:href=\"#DejaVuSans-2e\" x=\"63.623047\"/>\n",
-       "       <use xlink:href=\"#DejaVuSans-30\" x=\"95.410156\"/>\n",
-       "      </g>\n",
-       "     </g>\n",
-       "    </g>\n",
-       "    <g id=\"xtick_2\">\n",
-       "     <g id=\"line2d_2\">\n",
-       "      <g>\n",
-       "       <use xlink:href=\"#m361cdeea3f\" x=\"111.266761\" y=\"273.312\" style=\"stroke: #000000; stroke-width: 0.8\"/>\n",
-       "      </g>\n",
-       "     </g>\n",
-       "     <g id=\"text_2\">\n",
-       "      <!-- 0.2 -->\n",
-       "      <g transform=\"translate(103.315199 287.910437)scale(0.1 -0.1)\">\n",
-       "       <defs>\n",
-       "        <path id=\"DejaVuSans-32\" d=\"M 1228 531 \n",
-       "L 3431 531 \n",
-       "L 3431 0 \n",
-       "L 469 0 \n",
-       "L 469 531 \n",
-       "Q 828 903 1448 1529 \n",
-       "Q 2069 2156 2228 2338 \n",
-       "Q 2531 2678 2651 2914 \n",
-       "Q 2772 3150 2772 3378 \n",
-       "Q 2772 3750 2511 3984 \n",
-       "Q 2250 4219 1831 4219 \n",
-       "Q 1534 4219 1204 4116 \n",
-       "Q 875 4013 500 3803 \n",
-       "L 500 4441 \n",
-       "Q 881 4594 1212 4672 \n",
-       "Q 1544 4750 1819 4750 \n",
-       "Q 2544 4750 2975 4387 \n",
-       "Q 3406 4025 3406 3419 \n",
-       "Q 3406 3131 3298 2873 \n",
-       "Q 3191 2616 2906 2266 \n",
-       "Q 2828 2175 2409 1742 \n",
-       "Q 1991 1309 1228 531 \n",
-       "z\n",
-       "\" transform=\"scale(0.015625)\"/>\n",
-       "       </defs>\n",
-       "       <use xlink:href=\"#DejaVuSans-30\"/>\n",
-       "       <use xlink:href=\"#DejaVuSans-2e\" x=\"63.623047\"/>\n",
-       "       <use xlink:href=\"#DejaVuSans-32\" x=\"95.410156\"/>\n",
-       "      </g>\n",
-       "     </g>\n",
-       "    </g>\n",
-       "    <g id=\"xtick_3\">\n",
-       "     <g id=\"line2d_3\">\n",
-       "      <g>\n",
-       "       <use xlink:href=\"#m361cdeea3f\" x=\"176.19767\" y=\"273.312\" style=\"stroke: #000000; stroke-width: 0.8\"/>\n",
-       "      </g>\n",
-       "     </g>\n",
-       "     <g id=\"text_3\">\n",
-       "      <!-- 0.4 -->\n",
-       "      <g transform=\"translate(168.246108 287.910437)scale(0.1 -0.1)\">\n",
-       "       <defs>\n",
-       "        <path id=\"DejaVuSans-34\" d=\"M 2419 4116 \n",
-       "L 825 1625 \n",
-       "L 2419 1625 \n",
-       "L 2419 4116 \n",
-       "z\n",
-       "M 2253 4666 \n",
-       "L 3047 4666 \n",
-       "L 3047 1625 \n",
-       "L 3713 1625 \n",
-       "L 3713 1100 \n",
-       "L 3047 1100 \n",
-       "L 3047 0 \n",
-       "L 2419 0 \n",
-       "L 2419 1100 \n",
-       "L 313 1100 \n",
-       "L 313 1709 \n",
-       "L 2253 4666 \n",
-       "z\n",
-       "\" transform=\"scale(0.015625)\"/>\n",
-       "       </defs>\n",
-       "       <use xlink:href=\"#DejaVuSans-30\"/>\n",
-       "       <use xlink:href=\"#DejaVuSans-2e\" x=\"63.623047\"/>\n",
-       "       <use xlink:href=\"#DejaVuSans-34\" x=\"95.410156\"/>\n",
-       "      </g>\n",
-       "     </g>\n",
-       "    </g>\n",
-       "    <g id=\"xtick_4\">\n",
-       "     <g id=\"line2d_4\">\n",
-       "      <g>\n",
-       "       <use xlink:href=\"#m361cdeea3f\" x=\"241.12858\" y=\"273.312\" style=\"stroke: #000000; stroke-width: 0.8\"/>\n",
-       "      </g>\n",
-       "     </g>\n",
-       "     <g id=\"text_4\">\n",
-       "      <!-- 0.6 -->\n",
-       "      <g transform=\"translate(233.177017 287.910437)scale(0.1 -0.1)\">\n",
-       "       <defs>\n",
-       "        <path id=\"DejaVuSans-36\" d=\"M 2113 2584 \n",
-       "Q 1688 2584 1439 2293 \n",
-       "Q 1191 2003 1191 1497 \n",
-       "Q 1191 994 1439 701 \n",
-       "Q 1688 409 2113 409 \n",
-       "Q 2538 409 2786 701 \n",
-       "Q 3034 994 3034 1497 \n",
-       "Q 3034 2003 2786 2293 \n",
-       "Q 2538 2584 2113 2584 \n",
-       "z\n",
-       "M 3366 4563 \n",
-       "L 3366 3988 \n",
-       "Q 3128 4100 2886 4159 \n",
-       "Q 2644 4219 2406 4219 \n",
-       "Q 1781 4219 1451 3797 \n",
-       "Q 1122 3375 1075 2522 \n",
-       "Q 1259 2794 1537 2939 \n",
-       "Q 1816 3084 2150 3084 \n",
-       "Q 2853 3084 3261 2657 \n",
-       "Q 3669 2231 3669 1497 \n",
-       "Q 3669 778 3244 343 \n",
-       "Q 2819 -91 2113 -91 \n",
-       "Q 1303 -91 875 529 \n",
-       "Q 447 1150 447 2328 \n",
-       "Q 447 3434 972 4092 \n",
-       "Q 1497 4750 2381 4750 \n",
-       "Q 2619 4750 2861 4703 \n",
-       "Q 3103 4656 3366 4563 \n",
-       "z\n",
-       "\" transform=\"scale(0.015625)\"/>\n",
-       "       </defs>\n",
-       "       <use xlink:href=\"#DejaVuSans-30\"/>\n",
-       "       <use xlink:href=\"#DejaVuSans-2e\" x=\"63.623047\"/>\n",
-       "       <use xlink:href=\"#DejaVuSans-36\" x=\"95.410156\"/>\n",
-       "      </g>\n",
-       "     </g>\n",
-       "    </g>\n",
-       "    <g id=\"xtick_5\">\n",
-       "     <g id=\"line2d_5\">\n",
-       "      <g>\n",
-       "       <use xlink:href=\"#m361cdeea3f\" x=\"306.059489\" y=\"273.312\" style=\"stroke: #000000; stroke-width: 0.8\"/>\n",
-       "      </g>\n",
-       "     </g>\n",
-       "     <g id=\"text_5\">\n",
-       "      <!-- 0.8 -->\n",
-       "      <g transform=\"translate(298.107926 287.910437)scale(0.1 -0.1)\">\n",
-       "       <defs>\n",
-       "        <path id=\"DejaVuSans-38\" d=\"M 2034 2216 \n",
-       "Q 1584 2216 1326 1975 \n",
-       "Q 1069 1734 1069 1313 \n",
-       "Q 1069 891 1326 650 \n",
-       "Q 1584 409 2034 409 \n",
-       "Q 2484 409 2743 651 \n",
-       "Q 3003 894 3003 1313 \n",
-       "Q 3003 1734 2745 1975 \n",
-       "Q 2488 2216 2034 2216 \n",
-       "z\n",
-       "M 1403 2484 \n",
-       "Q 997 2584 770 2862 \n",
-       "Q 544 3141 544 3541 \n",
-       "Q 544 4100 942 4425 \n",
-       "Q 1341 4750 2034 4750 \n",
-       "Q 2731 4750 3128 4425 \n",
-       "Q 3525 4100 3525 3541 \n",
-       "Q 3525 3141 3298 2862 \n",
-       "Q 3072 2584 2669 2484 \n",
-       "Q 3125 2378 3379 2068 \n",
-       "Q 3634 1759 3634 1313 \n",
-       "Q 3634 634 3220 271 \n",
-       "Q 2806 -91 2034 -91 \n",
-       "Q 1263 -91 848 271 \n",
-       "Q 434 634 434 1313 \n",
-       "Q 434 1759 690 2068 \n",
-       "Q 947 2378 1403 2484 \n",
-       "z\n",
-       "M 1172 3481 \n",
-       "Q 1172 3119 1398 2916 \n",
-       "Q 1625 2713 2034 2713 \n",
-       "Q 2441 2713 2670 2916 \n",
-       "Q 2900 3119 2900 3481 \n",
-       "Q 2900 3844 2670 4047 \n",
-       "Q 2441 4250 2034 4250 \n",
-       "Q 1625 4250 1398 4047 \n",
-       "Q 1172 3844 1172 3481 \n",
-       "z\n",
-       "\" transform=\"scale(0.015625)\"/>\n",
-       "       </defs>\n",
-       "       <use xlink:href=\"#DejaVuSans-30\"/>\n",
-       "       <use xlink:href=\"#DejaVuSans-2e\" x=\"63.623047\"/>\n",
-       "       <use xlink:href=\"#DejaVuSans-38\" x=\"95.410156\"/>\n",
-       "      </g>\n",
-       "     </g>\n",
-       "    </g>\n",
-       "    <g id=\"xtick_6\">\n",
-       "     <g id=\"line2d_6\">\n",
-       "      <g>\n",
-       "       <use xlink:href=\"#m361cdeea3f\" x=\"370.990398\" y=\"273.312\" style=\"stroke: #000000; stroke-width: 0.8\"/>\n",
-       "      </g>\n",
-       "     </g>\n",
-       "     <g id=\"text_6\">\n",
-       "      <!-- 1.0 -->\n",
-       "      <g transform=\"translate(363.038835 287.910437)scale(0.1 -0.1)\">\n",
-       "       <defs>\n",
-       "        <path id=\"DejaVuSans-31\" d=\"M 794 531 \n",
-       "L 1825 531 \n",
-       "L 1825 4091 \n",
-       "L 703 3866 \n",
-       "L 703 4441 \n",
-       "L 1819 4666 \n",
-       "L 2450 4666 \n",
-       "L 2450 531 \n",
-       "L 3481 531 \n",
-       "L 3481 0 \n",
-       "L 794 0 \n",
-       "L 794 531 \n",
-       "z\n",
-       "\" transform=\"scale(0.015625)\"/>\n",
-       "       </defs>\n",
-       "       <use xlink:href=\"#DejaVuSans-31\"/>\n",
-       "       <use xlink:href=\"#DejaVuSans-2e\" x=\"63.623047\"/>\n",
-       "       <use xlink:href=\"#DejaVuSans-30\" x=\"95.410156\"/>\n",
-       "      </g>\n",
-       "     </g>\n",
-       "    </g>\n",
-       "   </g>\n",
-       "   <g id=\"matplotlib.axis_2\">\n",
-       "    <g id=\"ytick_1\">\n",
-       "     <g id=\"line2d_7\">\n",
-       "      <defs>\n",
-       "       <path id=\"m10b54568e8\" d=\"M 0 0 \n",
-       "L -3.5 0 \n",
-       "\" style=\"stroke: #000000; stroke-width: 0.8\"/>\n",
-       "      </defs>\n",
-       "      <g>\n",
-       "       <use xlink:href=\"#m10b54568e8\" x=\"30.103125\" y=\"261.216\" style=\"stroke: #000000; stroke-width: 0.8\"/>\n",
-       "      </g>\n",
-       "     </g>\n",
-       "     <g id=\"text_7\">\n",
-       "      <!-- 1.0 -->\n",
-       "      <g transform=\"translate(7.2 265.015219)scale(0.1 -0.1)\">\n",
-       "       <use xlink:href=\"#DejaVuSans-31\"/>\n",
-       "       <use xlink:href=\"#DejaVuSans-2e\" x=\"63.623047\"/>\n",
-       "       <use xlink:href=\"#DejaVuSans-30\" x=\"95.410156\"/>\n",
-       "      </g>\n",
-       "     </g>\n",
-       "    </g>\n",
-       "    <g id=\"ytick_2\">\n",
-       "     <g id=\"line2d_8\">\n",
-       "      <g>\n",
-       "       <use xlink:href=\"#m10b54568e8\" x=\"30.103125\" y=\"212.832\" style=\"stroke: #000000; stroke-width: 0.8\"/>\n",
-       "      </g>\n",
-       "     </g>\n",
-       "     <g id=\"text_8\">\n",
-       "      <!-- 1.2 -->\n",
-       "      <g transform=\"translate(7.2 216.631219)scale(0.1 -0.1)\">\n",
-       "       <use xlink:href=\"#DejaVuSans-31\"/>\n",
-       "       <use xlink:href=\"#DejaVuSans-2e\" x=\"63.623047\"/>\n",
-       "       <use xlink:href=\"#DejaVuSans-32\" x=\"95.410156\"/>\n",
-       "      </g>\n",
-       "     </g>\n",
-       "    </g>\n",
-       "    <g id=\"ytick_3\">\n",
-       "     <g id=\"line2d_9\">\n",
-       "      <g>\n",
-       "       <use xlink:href=\"#m10b54568e8\" x=\"30.103125\" y=\"164.448\" style=\"stroke: #000000; stroke-width: 0.8\"/>\n",
-       "      </g>\n",
-       "     </g>\n",
-       "     <g id=\"text_9\">\n",
-       "      <!-- 1.4 -->\n",
-       "      <g transform=\"translate(7.2 168.247219)scale(0.1 -0.1)\">\n",
-       "       <use xlink:href=\"#DejaVuSans-31\"/>\n",
-       "       <use xlink:href=\"#DejaVuSans-2e\" x=\"63.623047\"/>\n",
-       "       <use xlink:href=\"#DejaVuSans-34\" x=\"95.410156\"/>\n",
-       "      </g>\n",
-       "     </g>\n",
-       "    </g>\n",
-       "    <g id=\"ytick_4\">\n",
-       "     <g id=\"line2d_10\">\n",
-       "      <g>\n",
-       "       <use xlink:href=\"#m10b54568e8\" x=\"30.103125\" y=\"116.064\" style=\"stroke: #000000; stroke-width: 0.8\"/>\n",
-       "      </g>\n",
-       "     </g>\n",
-       "     <g id=\"text_10\">\n",
-       "      <!-- 1.6 -->\n",
-       "      <g transform=\"translate(7.2 119.863219)scale(0.1 -0.1)\">\n",
-       "       <use xlink:href=\"#DejaVuSans-31\"/>\n",
-       "       <use xlink:href=\"#DejaVuSans-2e\" x=\"63.623047\"/>\n",
-       "       <use xlink:href=\"#DejaVuSans-36\" x=\"95.410156\"/>\n",
-       "      </g>\n",
-       "     </g>\n",
-       "    </g>\n",
-       "    <g id=\"ytick_5\">\n",
-       "     <g id=\"line2d_11\">\n",
-       "      <g>\n",
-       "       <use xlink:href=\"#m10b54568e8\" x=\"30.103125\" y=\"67.68\" style=\"stroke: #000000; stroke-width: 0.8\"/>\n",
-       "      </g>\n",
-       "     </g>\n",
-       "     <g id=\"text_11\">\n",
-       "      <!-- 1.8 -->\n",
-       "      <g transform=\"translate(7.2 71.479219)scale(0.1 -0.1)\">\n",
-       "       <use xlink:href=\"#DejaVuSans-31\"/>\n",
-       "       <use xlink:href=\"#DejaVuSans-2e\" x=\"63.623047\"/>\n",
-       "       <use xlink:href=\"#DejaVuSans-38\" x=\"95.410156\"/>\n",
-       "      </g>\n",
-       "     </g>\n",
-       "    </g>\n",
-       "    <g id=\"ytick_6\">\n",
-       "     <g id=\"line2d_12\">\n",
-       "      <g>\n",
-       "       <use xlink:href=\"#m10b54568e8\" x=\"30.103125\" y=\"19.296\" style=\"stroke: #000000; stroke-width: 0.8\"/>\n",
-       "      </g>\n",
-       "     </g>\n",
-       "     <g id=\"text_12\">\n",
-       "      <!-- 2.0 -->\n",
-       "      <g transform=\"translate(7.2 23.095219)scale(0.1 -0.1)\">\n",
-       "       <use xlink:href=\"#DejaVuSans-32\"/>\n",
-       "       <use xlink:href=\"#DejaVuSans-2e\" x=\"63.623047\"/>\n",
-       "       <use xlink:href=\"#DejaVuSans-30\" x=\"95.410156\"/>\n",
-       "      </g>\n",
-       "     </g>\n",
-       "    </g>\n",
-       "   </g>\n",
-       "   <g id=\"line2d_13\">\n",
-       "    <path d=\"M 46.335852 261.216 \n",
-       "L 370.990398 19.296 \n",
-       "\" clip-path=\"url(#pa153dc7131)\" style=\"fill: none; stroke: #1f77b4; stroke-width: 1.5; stroke-linecap: square\"/>\n",
-       "   </g>\n",
-       "   <g id=\"patch_3\">\n",
-       "    <path d=\"M 30.103125 273.312 \n",
-       "L 30.103125 7.2 \n",
-       "\" style=\"fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square\"/>\n",
-       "   </g>\n",
-       "   <g id=\"patch_4\">\n",
-       "    <path d=\"M 387.223125 273.312 \n",
-       "L 387.223125 7.2 \n",
-       "\" style=\"fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square\"/>\n",
-       "   </g>\n",
-       "   <g id=\"patch_5\">\n",
-       "    <path d=\"M 30.103125 273.312 \n",
-       "L 387.223125 273.312 \n",
-       "\" style=\"fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square\"/>\n",
-       "   </g>\n",
-       "   <g id=\"patch_6\">\n",
-       "    <path d=\"M 30.103125 7.2 \n",
-       "L 387.223125 7.2 \n",
-       "\" style=\"fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square\"/>\n",
-       "   </g>\n",
-       "  </g>\n",
-       " </g>\n",
-       " <defs>\n",
-       "  <clipPath id=\"pa153dc7131\">\n",
-       "   <rect x=\"30.103125\" y=\"7.2\" width=\"357.12\" height=\"266.112\"/>\n",
-       "  </clipPath>\n",
-       " </defs>\n",
-       "</svg>\n"
-      ],
-      "text/plain": [
-       "<Figure size 640x480 with 1 Axes>"
-      ]
-     },
-     "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 45deb10f..2887211f 100644
--- a/nbconvert/tests/test_nbconvertapp.py
+++ b/nbconvert/tests/test_nbconvertapp.py
@@ -335,13 +335,13 @@ class TestNbConvertApp(TestsBase):
             assert os.path.isfile("notebook1.html")
             with open("notebook1.html", encoding="utf8") as f:
                 text = f.read()
-                assert "In&nbsp;[" not in text
+                assert "In\xa0[" not in text
                 assert "Out[6]" not in text
             self.nbconvert("notebook1.ipynb --log-level 0 --to html")
             assert os.path.isfile("notebook1.html")
             with open("notebook1.html", encoding="utf8") as f:
                 text2 = f.read()
-                assert "In&nbsp;[" in text2
+                assert "In\xa0[" in text2
                 assert "Out[6]" in text2
 
     def test_cell_tag_output(self):
@@ -369,7 +369,7 @@ class TestNbConvertApp(TestsBase):
             '<span class="o">=</span> '
             '<span class="n">symbols</span>'
             '<span class="p">(</span>'
-            '<span class="s1">&#39;x y z&#39;</span>'
+            '<span class="s1">\'x y z\'</span>'
             '<span class="p">)</span>'
         )
         for no_input_flag in (False, True):
@@ -382,7 +382,7 @@ class TestNbConvertApp(TestsBase):
 
                 with open("notebook1.html", encoding="utf8") as f:
                     text = f.read()
-                    assert no_input_flag == ("In&nbsp;[" not in text)
+                    assert no_input_flag == ("In\xa0[" not in text)
                     assert no_input_flag == ("Out[6]" not in text)
                     assert no_input_flag == (input_content_html not in text)
 
@@ -580,7 +580,7 @@ class TestNbConvertApp(TestsBase):
             with open("notebook5_embed_images.html", encoding="utf8") as f:
                 text = f.read()
                 assert "./containerized_deployments.jpeg" in text
-                assert "src='./containerized_deployments.jpeg'" in text
+                assert 'src="./containerized_deployments.jpeg"' in text
                 assert text.count("data:image/jpeg;base64") == 0
 
     def test_embedding_images_htmlexporter(self):
@@ -597,19 +597,6 @@ class TestNbConvertApp(TestsBase):
                 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 '<g id="line2d_2">' in text  # Must not be escaped
-                # TODO: these currently break...
-                # assert '<use xlink:href=\"#m361cdeea3f\"' in text  # Must not be escaped
-                assert "<!-- 1.6 -->" in text  # Must not be stripped
-
     def test_execute_widgets_from_nbconvert(self):
         """Check jupyter widgets render"""
         notebook_name = "Unexecuted_widget"