You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

319 lines
11 KiB

4 years ago
  1. # Copyright (c) Jupyter Development Team.
  2. # Distributed under the terms of the Modified BSD License.
  3. #
  4. #
  5. # Parts of this code is from IPyVolume (24.05.2017), used here under
  6. # this copyright and license with permission from the author
  7. # (see https://github.com/jupyter-widgets/ipywidgets/pull/1387)
  8. """
  9. Functions for generating embeddable HTML/javascript of a widget.
  10. """
  11. import json
  12. import re
  13. from .widgets import Widget, DOMWidget
  14. from .widgets.widget_link import Link
  15. from .widgets.docutils import doc_subst
  16. from ._version import __html_manager_version__
  17. snippet_template = u"""
  18. {load}
  19. <script type="application/vnd.jupyter.widget-state+json">
  20. {json_data}
  21. </script>
  22. {widget_views}
  23. """
  24. load_template = u"""<script src="{embed_url}"{use_cors}></script>"""
  25. load_requirejs_template = u"""
  26. <!-- Load require.js. Delete this if your page already loads require.js -->
  27. <script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.4/require.min.js" integrity="sha256-Ae2Vz/4ePdIu6ZyI/5ZGsYnb+m0JlOmKPjt6XZ9JJkA=" crossorigin="anonymous"></script>
  28. <script src="{embed_url}"{use_cors}></script>
  29. """
  30. requirejs_snippet_template = u"""
  31. <script type="application/vnd.jupyter.widget-state+json">
  32. {json_data}
  33. </script>
  34. {widget_views}
  35. """
  36. html_template = u"""<!DOCTYPE html>
  37. <html lang="en">
  38. <head>
  39. <meta charset="UTF-8">
  40. <title>{title}</title>
  41. </head>
  42. <body>
  43. {snippet}
  44. </body>
  45. </html>
  46. """
  47. widget_view_template = u"""<script type="application/vnd.jupyter.widget-view+json">
  48. {view_spec}
  49. </script>"""
  50. DEFAULT_EMBED_SCRIPT_URL = u'https://unpkg.com/@jupyter-widgets/html-manager@%s/dist/embed.js'%__html_manager_version__
  51. DEFAULT_EMBED_REQUIREJS_URL = u'https://unpkg.com/@jupyter-widgets/html-manager@%s/dist/embed-amd.js'%__html_manager_version__
  52. _doc_snippets = {}
  53. _doc_snippets['views_attribute'] = """
  54. views: widget or collection of widgets or None
  55. The widgets to include views for. If None, all DOMWidgets are
  56. included (not just the displayed ones).
  57. """
  58. _doc_snippets['embed_kwargs'] = """
  59. drop_defaults: boolean
  60. Whether to drop default values from the widget states.
  61. state: dict or None (default)
  62. The state to include. When set to None, the state of all widgets
  63. know to the widget manager is included. Otherwise it uses the
  64. passed state directly. This allows for end users to include a
  65. smaller state, under the responsibility that this state is
  66. sufficient to reconstruct the embedded views.
  67. indent: integer, string or None
  68. The indent to use for the JSON state dump. See `json.dumps` for
  69. full description.
  70. embed_url: string or None
  71. Allows for overriding the URL used to fetch the widget manager
  72. for the embedded code. This defaults (None) to an `unpkg` CDN url.
  73. requirejs: boolean (True)
  74. Enables the requirejs-based embedding, which allows for custom widgets.
  75. If True, the embed_url should point to an AMD module.
  76. cors: boolean (True)
  77. If True avoids sending user credentials while requesting the scripts.
  78. When opening an HTML file from disk, some browsers may refuse to load
  79. the scripts.
  80. """
  81. def _find_widget_refs_by_state(widget, state):
  82. """Find references to other widgets in a widget's state"""
  83. # Copy keys to allow changes to state during iteration:
  84. keys = tuple(state.keys())
  85. for key in keys:
  86. value = getattr(widget, key)
  87. # Trivial case: Direct references to other widgets:
  88. if isinstance(value, Widget):
  89. yield value
  90. # Also check for buried references in known, JSON-able structures
  91. # Note: This might miss references buried in more esoteric structures
  92. elif isinstance(value, (list, tuple)):
  93. for item in value:
  94. if isinstance(item, Widget):
  95. yield item
  96. elif isinstance(value, dict):
  97. for item in value.values():
  98. if isinstance(item, Widget):
  99. yield item
  100. def _get_recursive_state(widget, store=None, drop_defaults=False):
  101. """Gets the embed state of a widget, and all other widgets it refers to as well"""
  102. if store is None:
  103. store = dict()
  104. state = widget._get_embed_state(drop_defaults=drop_defaults)
  105. store[widget.model_id] = state
  106. # Loop over all values included in state (i.e. don't consider excluded values):
  107. for ref in _find_widget_refs_by_state(widget, state['state']):
  108. if ref.model_id not in store:
  109. _get_recursive_state(ref, store, drop_defaults=drop_defaults)
  110. return store
  111. def add_resolved_links(store, drop_defaults):
  112. """Adds the state of any link models between two models in store"""
  113. for widget_id, widget in Widget.widgets.items(): # go over all widgets
  114. if isinstance(widget, Link) and widget_id not in store:
  115. if widget.source[0].model_id in store and widget.target[0].model_id in store:
  116. store[widget.model_id] = widget._get_embed_state(drop_defaults=drop_defaults)
  117. def dependency_state(widgets, drop_defaults=True):
  118. """Get the state of all widgets specified, and their dependencies.
  119. This uses a simple dependency finder, including:
  120. - any widget directly referenced in the state of an included widget
  121. - any widget in a list/tuple attribute in the state of an included widget
  122. - any widget in a dict attribute in the state of an included widget
  123. - any jslink/jsdlink between two included widgets
  124. What this alogrithm does not do:
  125. - Find widget references in nested list/dict structures
  126. - Find widget references in other types of attributes
  127. Note that this searches the state of the widgets for references, so if
  128. a widget reference is not included in the serialized state, it won't
  129. be considered as a dependency.
  130. Parameters
  131. ----------
  132. widgets: single widget or list of widgets.
  133. This function will return the state of every widget mentioned
  134. and of all their dependencies.
  135. drop_defaults: boolean
  136. Whether to drop default values from the widget states.
  137. Returns
  138. -------
  139. A dictionary with the state of the widgets and any widget they
  140. depend on.
  141. """
  142. # collect the state of all relevant widgets
  143. if widgets is None:
  144. # Get state of all widgets, no smart resolution needed.
  145. state = Widget.get_manager_state(drop_defaults=drop_defaults, widgets=None)['state']
  146. else:
  147. try:
  148. widgets[0]
  149. except (IndexError, TypeError):
  150. widgets = [widgets]
  151. state = {}
  152. for widget in widgets:
  153. _get_recursive_state(widget, state, drop_defaults)
  154. # Add any links between included widgets:
  155. add_resolved_links(state, drop_defaults)
  156. return state
  157. @doc_subst(_doc_snippets)
  158. def embed_data(views, drop_defaults=True, state=None):
  159. """Gets data for embedding.
  160. Use this to get the raw data for embedding if you have special
  161. formatting needs.
  162. Parameters
  163. ----------
  164. {views_attribute}
  165. drop_defaults: boolean
  166. Whether to drop default values from the widget states.
  167. state: dict or None (default)
  168. The state to include. When set to None, the state of all widgets
  169. know to the widget manager is included. Otherwise it uses the
  170. passed state directly. This allows for end users to include a
  171. smaller state, under the responsibility that this state is
  172. sufficient to reconstruct the embedded views.
  173. Returns
  174. -------
  175. A dictionary with the following entries:
  176. manager_state: dict of the widget manager state data
  177. view_specs: a list of widget view specs
  178. """
  179. if views is None:
  180. views = [w for w in Widget.widgets.values() if isinstance(w, DOMWidget)]
  181. else:
  182. try:
  183. views[0]
  184. except (IndexError, TypeError):
  185. views = [views]
  186. if state is None:
  187. # Get state of all known widgets
  188. state = Widget.get_manager_state(drop_defaults=drop_defaults, widgets=None)['state']
  189. # Rely on ipywidget to get the default values
  190. json_data = Widget.get_manager_state(widgets=[])
  191. # but plug in our own state
  192. json_data['state'] = state
  193. view_specs = [w.get_view_spec() for w in views]
  194. return dict(manager_state=json_data, view_specs=view_specs)
  195. script_escape_re = re.compile(r'<(script|/script|!--)', re.IGNORECASE)
  196. def escape_script(s):
  197. """Escape a string that will be the content of an HTML script tag.
  198. We replace the opening bracket of <script, </script, and <!-- with the unicode
  199. equivalent. This is inspired by the documentation for the script tag at
  200. https://html.spec.whatwg.org/multipage/scripting.html#restrictions-for-contents-of-script-elements
  201. We only replace these three cases so that most html or other content
  202. involving `<` is readable.
  203. """
  204. return script_escape_re.sub(r'\\u003c\1', s)
  205. @doc_subst(_doc_snippets)
  206. def embed_snippet(views,
  207. drop_defaults=True,
  208. state=None,
  209. indent=2,
  210. embed_url=None,
  211. requirejs=True,
  212. cors=True
  213. ):
  214. """Return a snippet that can be embedded in an HTML file.
  215. Parameters
  216. ----------
  217. {views_attribute}
  218. {embed_kwargs}
  219. Returns
  220. -------
  221. A unicode string with an HTML snippet containing several `<script>` tags.
  222. """
  223. data = embed_data(views, drop_defaults=drop_defaults, state=state)
  224. widget_views = u'\n'.join(
  225. widget_view_template.format(view_spec=escape_script(json.dumps(view_spec)))
  226. for view_spec in data['view_specs']
  227. )
  228. if embed_url is None:
  229. embed_url = DEFAULT_EMBED_REQUIREJS_URL if requirejs else DEFAULT_EMBED_SCRIPT_URL
  230. load = load_requirejs_template if requirejs else load_template
  231. use_cors = ' crossorigin="anonymous"' if cors else ''
  232. values = {
  233. 'load': load.format(embed_url=embed_url, use_cors=use_cors),
  234. 'json_data': escape_script(json.dumps(data['manager_state'], indent=indent)),
  235. 'widget_views': widget_views,
  236. }
  237. return snippet_template.format(**values)
  238. @doc_subst(_doc_snippets)
  239. def embed_minimal_html(fp, views, title=u'IPyWidget export', template=None, **kwargs):
  240. """Write a minimal HTML file with widget views embedded.
  241. Parameters
  242. ----------
  243. fp: filename or file-like object
  244. The file to write the HTML output to.
  245. {views_attribute}
  246. title: title of the html page.
  247. template: Template in which to embed the widget state.
  248. This should be a Python string with placeholders
  249. `{{title}}` and `{{snippet}}`. The `{{snippet}}` placeholder
  250. will be replaced by all the widgets.
  251. {embed_kwargs}
  252. """
  253. snippet = embed_snippet(views, **kwargs)
  254. values = {
  255. 'title': title,
  256. 'snippet': snippet,
  257. }
  258. if template is None:
  259. template = html_template
  260. html_code = template.format(**values)
  261. # Check if fp is writable:
  262. if hasattr(fp, 'write'):
  263. fp.write(html_code)
  264. else:
  265. # Assume fp is a filename:
  266. with open(fp, "w") as f:
  267. f.write(html_code)