Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/nbconvert/exporters/html.py: 32%
144 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-01 06:54 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-01 06:54 +0000
1"""HTML Exporter class"""
3# Copyright (c) Jupyter Development Team.
4# Distributed under the terms of the Modified BSD License.
6import base64
7import json
8import mimetypes
9import os
10from pathlib import Path
11from typing import Any, Dict, Optional, Tuple
13import jinja2
14import markupsafe
15from jupyter_core.paths import jupyter_path
16from traitlets import Bool, Unicode, default
17from traitlets.config import Config
19if tuple(int(x) for x in jinja2.__version__.split(".")[:3]) < (3, 0, 0):
20 from jinja2 import contextfilter # type:ignore
21else:
22 from jinja2 import pass_context as contextfilter
24from jinja2.loaders import split_template_path
25from nbformat import NotebookNode
27from nbconvert.filters.highlight import Highlight2HTML
28from nbconvert.filters.markdown_mistune import IPythonRenderer, MarkdownWithMath
29from nbconvert.filters.widgetsdatatypefilter import WidgetsDataTypeFilter
31from .templateexporter import TemplateExporter
34def find_lab_theme(theme_name):
35 """
36 Find a JupyterLab theme location by name.
38 Parameters
39 ----------
40 theme_name : str
41 The name of the labextension theme you want to find.
43 Raises
44 ------
45 ValueError
46 If the theme was not found, or if it was not specific enough.
48 Returns
49 -------
50 theme_name: str
51 Full theme name (with scope, if any)
52 labextension_path : Path
53 The path to the found labextension on the system.
54 """
55 paths = jupyter_path("labextensions")
57 matching_themes = []
58 theme_path = None
59 for path in paths:
60 for dirpath, dirnames, filenames in os.walk(path):
61 # If it's a federated labextension that contains themes
62 if "package.json" in filenames and "themes" in dirnames:
63 # TODO Find the theme name in the JS code instead?
64 # TODO Find if it's a light or dark theme?
65 with open(Path(dirpath) / "package.json", encoding="utf-8") as fobj:
66 labext_name = json.loads(fobj.read())["name"]
68 if labext_name == theme_name or theme_name in labext_name.split("/"):
69 matching_themes.append(labext_name)
71 full_theme_name = labext_name
72 theme_path = Path(dirpath) / "themes" / labext_name
74 if len(matching_themes) == 0:
75 msg = f'Could not find lab theme "{theme_name}"'
76 raise ValueError(msg)
78 if len(matching_themes) > 1:
79 msg = (
80 f'Found multiple themes matching "{theme_name}": {matching_themes}. '
81 "Please be more specific about which theme you want to use."
82 )
83 raise ValueError(msg)
85 return full_theme_name, theme_path
88class HTMLExporter(TemplateExporter):
89 """
90 Exports a basic HTML document. This exporter assists with the export of
91 HTML. Inherit from it if you are writing your own HTML template and need
92 custom preprocessors/filters. If you don't need custom preprocessors/
93 filters, just change the 'template_file' config option.
94 """
96 export_from_notebook = "HTML"
98 anchor_link_text = Unicode("¶", help="The text used as the text for anchor links.").tag(
99 config=True
100 )
102 exclude_anchor_links = Bool(False, help="If anchor links should be included or not.").tag(
103 config=True
104 )
106 require_js_url = Unicode(
107 "https://cdnjs.cloudflare.com/ajax/libs/require.js/2.1.10/require.min.js",
108 help="""
109 URL to load require.js from.
111 Defaults to loading from cdnjs.
112 """,
113 ).tag(config=True)
115 mathjax_url = Unicode(
116 "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/latest.js?config=TeX-AMS_CHTML-full,Safe",
117 help="""
118 URL to load Mathjax from.
120 Defaults to loading from cdnjs.
121 """,
122 ).tag(config=True)
124 mermaid_js_url = Unicode(
125 "https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.2.3/mermaid.esm.min.mjs",
126 help="""
127 URL to load MermaidJS from.
129 Defaults to loading from cdnjs.
130 """,
131 )
133 jquery_url = Unicode(
134 "https://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.3/jquery.min.js",
135 help="""
136 URL to load jQuery from.
138 Defaults to loading from cdnjs.
139 """,
140 ).tag(config=True)
142 jupyter_widgets_base_url = Unicode(
143 "https://unpkg.com/", help="URL base for Jupyter widgets"
144 ).tag(config=True)
146 widget_renderer_url = Unicode("", help="Full URL for Jupyter widgets").tag(config=True)
148 html_manager_semver_range = Unicode(
149 "*", help="Semver range for Jupyter widgets HTML manager"
150 ).tag(config=True)
152 @default("file_extension")
153 def _file_extension_default(self):
154 return ".html"
156 @default("template_name")
157 def _template_name_default(self):
158 return "lab"
160 theme = Unicode(
161 "light",
162 help="Template specific theme(e.g. the name of a JupyterLab CSS theme distributed as prebuilt extension for the lab template)",
163 ).tag(config=True)
165 sanitize_html = Bool(
166 False,
167 help=(
168 "Whether the HTML in Markdown cells and cell outputs should be sanitized."
169 "This should be set to True by nbviewer or similar tools."
170 ),
171 ).tag(config=True)
173 embed_images = Bool(
174 False, help="Whether or not to embed images as base64 in markdown cells."
175 ).tag(config=True)
177 output_mimetype = "text/html"
179 @property
180 def default_config(self):
181 c = Config(
182 {
183 "NbConvertBase": {
184 "display_data_priority": [
185 "application/vnd.jupyter.widget-view+json",
186 "application/javascript",
187 "text/html",
188 "text/markdown",
189 "image/svg+xml",
190 "text/latex",
191 "image/png",
192 "image/jpeg",
193 "text/plain",
194 ]
195 },
196 "HighlightMagicsPreprocessor": {"enabled": True},
197 }
198 )
199 if super().default_config:
200 c2 = super().default_config.copy()
201 c2.merge(c)
202 c = c2
203 return c
205 @contextfilter
206 def markdown2html(self, context, source):
207 """Markdown to HTML filter respecting the anchor_link_text setting"""
208 cell = context.get("cell", {})
209 attachments = cell.get("attachments", {})
210 path = context.get("resources", {}).get("metadata", {}).get("path", "")
212 renderer = IPythonRenderer(
213 escape=False,
214 attachments=attachments,
215 embed_images=self.embed_images,
216 path=path,
217 anchor_link_text=self.anchor_link_text,
218 exclude_anchor_links=self.exclude_anchor_links,
219 )
220 return MarkdownWithMath(renderer=renderer).render(source)
222 def default_filters(self):
223 """Get the default filters."""
224 yield from super().default_filters()
225 yield ("markdown2html", self.markdown2html)
227 def from_notebook_node( # type:ignore
228 self, nb: NotebookNode, resources: Optional[Dict] = None, **kw: Any
229 ) -> Tuple[str, Dict]:
230 """Convert from notebook node."""
231 langinfo = nb.metadata.get("language_info", {})
232 lexer = langinfo.get("pygments_lexer", langinfo.get("name", None))
233 highlight_code = self.filters.get(
234 "highlight_code", Highlight2HTML(pygments_lexer=lexer, parent=self)
235 )
237 filter_data_type = WidgetsDataTypeFilter(
238 notebook_metadata=self._nb_metadata, parent=self, resources=resources
239 )
241 self.register_filter("highlight_code", highlight_code)
242 self.register_filter("filter_data_type", filter_data_type)
243 return super().from_notebook_node(nb, resources, **kw)
245 def _init_resources(self, resources): # noqa
246 def resources_include_css(name):
247 env = self.environment
248 code = """<style type="text/css">\n%s</style>""" % (env.loader.get_source(env, name)[0])
249 return markupsafe.Markup(code)
251 def resources_include_lab_theme(name):
252 # Try to find the theme with the given name, looking through the labextensions
253 _, theme_path = find_lab_theme(name)
255 with open(theme_path / "index.css") as file:
256 data = file.read()
258 # Embed assets (fonts, images...)
259 for asset in os.listdir(theme_path):
260 local_url = f"url({Path(asset).as_posix()})"
262 if local_url in data:
263 mime_type = mimetypes.guess_type(asset)[0]
265 # Replace asset url by a base64 dataurl
266 with open(theme_path / asset, "rb") as assetfile:
267 base64_data = base64.b64encode(assetfile.read())
268 base64_str = base64_data.replace(b"\n", b"").decode("ascii")
270 data = data.replace(local_url, f"url(data:{mime_type};base64,{base64_str})")
272 code = """<style type="text/css">\n%s</style>""" % data
273 return markupsafe.Markup(code)
275 def resources_include_js(name):
276 """Get the resources include JS for a name."""
277 env = self.environment
278 code = """<script>\n%s</script>""" % (env.loader.get_source(env, name)[0])
279 return markupsafe.Markup(code)
281 def resources_include_url(name):
282 """Get the resources include url for a name."""
283 env = self.environment
284 mime_type, encoding = mimetypes.guess_type(name)
285 try:
286 # we try to load via the jinja loader, but that tries to load
287 # as (encoded) text
288 data = env.loader.get_source(env, name)[0].encode("utf8")
289 except UnicodeDecodeError:
290 # if that fails (for instance a binary file, png or ttf)
291 # we mimic jinja2
292 pieces = split_template_path(name)
293 for searchpath in self.template_paths:
294 filename = os.path.join(searchpath, *pieces)
295 if os.path.exists(filename):
296 with open(filename, "rb") as f:
297 data = f.read()
298 break
299 else:
300 msg = f"No file {name!r} found in {searchpath!r}"
301 raise ValueError(msg)
302 data = base64.b64encode(data)
303 data = data.replace(b"\n", b"").decode("ascii")
304 src = f"data:{mime_type};base64,{data}"
305 return markupsafe.Markup(src)
307 resources = super()._init_resources(resources)
308 resources["theme"] = self.theme
309 resources["include_css"] = resources_include_css
310 resources["include_lab_theme"] = resources_include_lab_theme
311 resources["include_js"] = resources_include_js
312 resources["include_url"] = resources_include_url
313 resources["require_js_url"] = self.require_js_url
314 resources["mathjax_url"] = self.mathjax_url
315 resources["mermaid_js_url"] = self.mermaid_js_url
316 resources["jquery_url"] = self.jquery_url
317 resources["jupyter_widgets_base_url"] = self.jupyter_widgets_base_url
318 resources["widget_renderer_url"] = self.widget_renderer_url
319 resources["html_manager_semver_range"] = self.html_manager_semver_range
320 resources["should_sanitize_html"] = self.sanitize_html
321 return resources