diff --git a/docs/conf.py b/docs/conf.py
index 4b3e34b3d..87de0eb87 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -58,7 +58,7 @@ sys.path.insert(0, os.path.abspath('sphinx_exts'))
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
-extensions = ['sphinx.ext.autodoc', 'sphinx_paramlinks', 'sphinxcontrib_jquery']
+extensions = ['sphinx.ext.autodoc', 'sphinx_paramlinks', 'sphinxcontrib_jquery', 'sphinx_copybutton']
if tags.has('spelling'): # type: ignore
extensions.append('sphinxcontrib.spelling')
diff --git a/docs/sphinx_exts/sphinx_copybutton/LICENSE b/docs/sphinx_exts/sphinx_copybutton/LICENSE
new file mode 100644
index 000000000..dab6ba48f
--- /dev/null
+++ b/docs/sphinx_exts/sphinx_copybutton/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 Chris Holdgraf
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/docs/sphinx_exts/sphinx_copybutton/README.md b/docs/sphinx_exts/sphinx_copybutton/README.md
new file mode 100644
index 000000000..714e5e229
--- /dev/null
+++ b/docs/sphinx_exts/sphinx_copybutton/README.md
@@ -0,0 +1,56 @@
+# sphinx-copybutton
+
+[](https://pypi.org/project/sphinx_copybutton/) | [](https://anaconda.org/conda-forge/sphinx-copybutton) | [](https://sphinx-copybutton.readthedocs.io/en/latest/?badge=latest)
+
+A small sphinx extension to add a "copy" button to code blocks.
+
+See [the sphinx-copybutton documentation](https://sphinx-copybutton.readthedocs.io/en/latest/) for more details!
+
+
+
+## Installation
+
+You can install `sphinx-copybutton` with `pip`:
+
+```bash
+pip install sphinx-copybutton
+```
+
+Or with `conda` via `conda-forge`:
+
+```bash
+conda install -c conda-forge sphinx-copybutton
+```
+
+
+## Usage
+
+In your `conf.py` configuration file, add `sphinx_copybutton` to your extensions list.
+E.g.:
+
+```python
+extensions = [
+ ...
+ 'sphinx_copybutton'
+ ...
+]
+```
+
+When you build your site, your code blocks should now have little copy buttons to their
+right. Clicking the button will copy the code inside!
+
+## Customization
+
+If you'd like to customize the look of the copy buttons, you can over-write any of the
+CSS rules specified in the Sphinx-CopyButton CSS file ([link](sphinx_copybutton/_static/copybutton.css))
+
+## Development
+
+Development should principally adhere to the [EBP Developer Conventions](https://github.com/executablebooks/.github/blob/master/CONTRIBUTING.md)
+
+Sphinx-Copybutton is [hosted on the pypi repository](https://pypi.org/project/sphinx-copybutton/).
+After a release - following the [EBP release instructions](https://github.com/executablebooks/.github/blob/master/CONTRIBUTING.md#releases-and-change-logs) - confirm that the new version of Sphinx-Copybutton [is posted to pypi](https://pypi.org/project/sphinx-copybutton/).
+
+## Acknowledgements
+
+Many thanks to the excellent [clipboard.js library](https://clipboardjs.com/) for the lightweight javascript code that powers the copy button!
diff --git a/docs/sphinx_exts/sphinx_copybutton/__init__.py b/docs/sphinx_exts/sphinx_copybutton/__init__.py
new file mode 100644
index 000000000..953e7f34d
--- /dev/null
+++ b/docs/sphinx_exts/sphinx_copybutton/__init__.py
@@ -0,0 +1,99 @@
+"""A small sphinx extension to add "copy" buttons to code blocks."""
+from pathlib import Path
+from sphinx.util import logging
+
+__version__ = "0.5.2"
+
+logger = logging.getLogger(__name__)
+
+
+def scb_static_path(app):
+ app.config.html_static_path.append(
+ str(Path(__file__).parent.joinpath("_static").absolute())
+ )
+
+
+def add_to_context(app, config):
+ # Update the global context
+ config.html_context.update(
+ {"copybutton_prompt_text": config.copybutton_prompt_text}
+ )
+ config.html_context.update(
+ {"copybutton_prompt_is_regexp": config.copybutton_prompt_is_regexp}
+ )
+ config.html_context.update(
+ {"copybutton_only_copy_prompt_lines": config.copybutton_only_copy_prompt_lines}
+ )
+ config.html_context.update(
+ {"copybutton_remove_prompts": config.copybutton_remove_prompts}
+ )
+ config.html_context.update(
+ {"copybutton_copy_empty_lines": config.copybutton_copy_empty_lines}
+ )
+ config.html_context.update(
+ {
+ "copybutton_line_continuation_character": (
+ config.copybutton_line_continuation_character
+ )
+ }
+ )
+ config.html_context.update(
+ {"copybutton_here_doc_delimiter": config.copybutton_here_doc_delimiter}
+ )
+
+ # Old image path deprecation
+ # REMOVE after next release
+ if config.copybutton_image_path:
+ path = Path(app.srcdir) / config.copybutton_image_path
+ logger.warning("copybutton_image_path is deprecated, use copybutton_image_svg")
+ if not path.exists():
+ raise ValueError("copybutton_img_path does not exist")
+ if not path.suffix == ".svg":
+ raise ValueError("copybutton_img_path must be an SVG")
+ config.copybutton_image_svg = path.read_text()
+
+ config.html_context.update({"copybutton_image_svg": config.copybutton_image_svg})
+ config.html_context.update({"copybutton_selector": config.copybutton_selector})
+ config.html_context.update(
+ {
+ "copybutton_format_func": Path(__file__)
+ .parent.joinpath("_static", "copybutton_funcs.js")
+ .read_text()
+ .replace("export function", "function")
+ }
+ )
+ config.html_context.update({"copybutton_exclude": config.copybutton_exclude})
+
+
+def setup(app):
+ logger.verbose("Adding copy buttons to code blocks...")
+ # Add our static path
+ app.connect("builder-inited", scb_static_path)
+
+ # configuration for this tool
+ app.add_config_value("copybutton_prompt_text", "", "html")
+ app.add_config_value("copybutton_prompt_is_regexp", False, "html")
+ app.add_config_value("copybutton_only_copy_prompt_lines", True, "html")
+ app.add_config_value("copybutton_remove_prompts", True, "html")
+ app.add_config_value("copybutton_copy_empty_lines", True, "html")
+ app.add_config_value("copybutton_line_continuation_character", "", "html")
+ app.add_config_value("copybutton_here_doc_delimiter", "", "html")
+ app.add_config_value("copybutton_image_svg", "", "html")
+ app.add_config_value("copybutton_selector", "div.highlight pre", "html")
+ app.add_config_value("copybutton_exclude", ".linenos", "html")
+
+ # DEPRECATE THIS AFTER THE NEXT RELEASE
+ app.add_config_value("copybutton_image_path", "", "html")
+
+ # Add configuration value to the template
+ app.connect("config-inited", add_to_context)
+
+ # Add relevant code to headers
+ app.add_css_file("copybutton.css")
+ app.add_js_file("clipboard.min.js")
+ app.add_js_file("copybutton.js")
+ return {
+ "version": __version__,
+ "parallel_read_safe": True,
+ "parallel_write_safe": True,
+ }
diff --git a/docs/sphinx_exts/sphinx_copybutton/_static/check-solid.svg b/docs/sphinx_exts/sphinx_copybutton/_static/check-solid.svg
new file mode 100644
index 000000000..92fad4b5c
--- /dev/null
+++ b/docs/sphinx_exts/sphinx_copybutton/_static/check-solid.svg
@@ -0,0 +1,4 @@
+
diff --git a/docs/sphinx_exts/sphinx_copybutton/_static/clipboard.min.js b/docs/sphinx_exts/sphinx_copybutton/_static/clipboard.min.js
new file mode 100644
index 000000000..54b3c4638
--- /dev/null
+++ b/docs/sphinx_exts/sphinx_copybutton/_static/clipboard.min.js
@@ -0,0 +1,7 @@
+/*!
+ * clipboard.js v2.0.8
+ * https://clipboardjs.com/
+ *
+ * Licensed MIT © Zeno Rocha
+ */
+!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return n={686:function(t,e,n){"use strict";n.d(e,{default:function(){return o}});var e=n(279),i=n.n(e),e=n(370),u=n.n(e),e=n(817),c=n.n(e);function a(t){try{return document.execCommand(t)}catch(t){return}}var f=function(t){t=c()(t);return a("cut"),t};var l=function(t){var e,n,o,r=1
+
+
+
+
diff --git a/docs/sphinx_exts/sphinx_copybutton/_static/copybutton.css b/docs/sphinx_exts/sphinx_copybutton/_static/copybutton.css
new file mode 100644
index 000000000..f1916ec7d
--- /dev/null
+++ b/docs/sphinx_exts/sphinx_copybutton/_static/copybutton.css
@@ -0,0 +1,94 @@
+/* Copy buttons */
+button.copybtn {
+ position: absolute;
+ display: flex;
+ top: .3em;
+ right: .3em;
+ width: 1.7em;
+ height: 1.7em;
+ opacity: 0;
+ transition: opacity 0.3s, border .3s, background-color .3s;
+ user-select: none;
+ padding: 0;
+ border: none;
+ outline: none;
+ border-radius: 0.4em;
+ /* The colors that GitHub uses */
+ border: #1b1f2426 1px solid;
+ background-color: #f6f8fa;
+ color: #57606a;
+}
+
+button.copybtn.success {
+ border-color: #22863a;
+ color: #22863a;
+}
+
+button.copybtn svg {
+ stroke: currentColor;
+ width: 1.5em;
+ height: 1.5em;
+ padding: 0.1em;
+}
+
+div.highlight {
+ position: relative;
+}
+
+/* Show the copybutton */
+.highlight:hover button.copybtn, button.copybtn.success {
+ opacity: 1;
+}
+
+.highlight button.copybtn:hover {
+ background-color: rgb(235, 235, 235);
+}
+
+.highlight button.copybtn:active {
+ background-color: rgb(187, 187, 187);
+}
+
+/**
+ * A minimal CSS-only tooltip copied from:
+ * https://codepen.io/mildrenben/pen/rVBrpK
+ *
+ * To use, write HTML like the following:
+ *
+ * Short
+ */
+ .o-tooltip--left {
+ position: relative;
+ }
+
+ .o-tooltip--left:after {
+ opacity: 0;
+ visibility: hidden;
+ position: absolute;
+ content: attr(data-tooltip);
+ padding: .2em;
+ font-size: .8em;
+ left: -.2em;
+ background: grey;
+ color: white;
+ white-space: nowrap;
+ z-index: 2;
+ border-radius: 2px;
+ transform: translateX(-102%) translateY(0);
+ transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1);
+}
+
+.o-tooltip--left:hover:after {
+ display: block;
+ opacity: 1;
+ visibility: visible;
+ transform: translateX(-100%) translateY(0);
+ transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1);
+ transition-delay: .5s;
+}
+
+/* By default the copy button shouldn't show up when printing a page */
+@media print {
+ button.copybtn {
+ display: none;
+ }
+}
diff --git a/docs/sphinx_exts/sphinx_copybutton/_static/copybutton.js_t b/docs/sphinx_exts/sphinx_copybutton/_static/copybutton.js_t
new file mode 100644
index 000000000..ec91f2096
--- /dev/null
+++ b/docs/sphinx_exts/sphinx_copybutton/_static/copybutton.js_t
@@ -0,0 +1,175 @@
+// Localization support
+const messages = {
+ 'en': {
+ 'copy': 'Copy',
+ 'copy_to_clipboard': 'Copy to clipboard',
+ 'copy_success': 'Copied!',
+ 'copy_failure': 'Failed to copy',
+ },
+ 'es' : {
+ 'copy': 'Copiar',
+ 'copy_to_clipboard': 'Copiar al portapapeles',
+ 'copy_success': '¡Copiado!',
+ 'copy_failure': 'Error al copiar',
+ },
+ 'de' : {
+ 'copy': 'Kopieren',
+ 'copy_to_clipboard': 'In die Zwischenablage kopieren',
+ 'copy_success': 'Kopiert!',
+ 'copy_failure': 'Fehler beim Kopieren',
+ },
+ 'fr' : {
+ 'copy': 'Copier',
+ 'copy_to_clipboard': 'Copier dans le presse-papier',
+ 'copy_success': 'Copié !',
+ 'copy_failure': 'Échec de la copie',
+ },
+ 'ru': {
+ 'copy': 'Скопировать',
+ 'copy_to_clipboard': 'Скопировать в буфер',
+ 'copy_success': 'Скопировано!',
+ 'copy_failure': 'Не удалось скопировать',
+ },
+ 'zh-CN': {
+ 'copy': '复制',
+ 'copy_to_clipboard': '复制到剪贴板',
+ 'copy_success': '复制成功!',
+ 'copy_failure': '复制失败',
+ },
+ 'it' : {
+ 'copy': 'Copiare',
+ 'copy_to_clipboard': 'Copiato negli appunti',
+ 'copy_success': 'Copiato!',
+ 'copy_failure': 'Errore durante la copia',
+ }
+}
+
+let locale = 'en'
+if( document.documentElement.lang !== undefined
+ && messages[document.documentElement.lang] !== undefined ) {
+ locale = document.documentElement.lang
+}
+
+let doc_url_root = DOCUMENTATION_OPTIONS.URL_ROOT;
+if (doc_url_root == '#') {
+ doc_url_root = '';
+}
+
+/**
+ * SVG files for our copy buttons
+ */
+let iconCheck = ``
+
+// If the user specified their own SVG use that, otherwise use the default
+let iconCopy = `{{ copybutton_image_svg }}`;
+if (!iconCopy) {
+ iconCopy = ``
+}
+
+/**
+ * Set up copy/paste for code blocks
+ */
+
+const runWhenDOMLoaded = cb => {
+ if (document.readyState != 'loading') {
+ cb()
+ } else if (document.addEventListener) {
+ document.addEventListener('DOMContentLoaded', cb)
+ } else {
+ document.attachEvent('onreadystatechange', function() {
+ if (document.readyState == 'complete') cb()
+ })
+ }
+}
+
+const codeCellId = index => `codecell${index}`
+
+// Clears selected text since ClipboardJS will select the text when copying
+const clearSelection = () => {
+ if (window.getSelection) {
+ window.getSelection().removeAllRanges()
+ } else if (document.selection) {
+ document.selection.empty()
+ }
+}
+
+// Changes tooltip text for a moment, then changes it back
+// We want the timeout of our `success` class to be a bit shorter than the
+// tooltip and icon change, so that we can hide the icon before changing back.
+var timeoutIcon = 2000;
+var timeoutSuccessClass = 1500;
+
+const temporarilyChangeTooltip = (el, oldText, newText) => {
+ el.setAttribute('data-tooltip', newText)
+ el.classList.add('success')
+ // Remove success a little bit sooner than we change the tooltip
+ // So that we can use CSS to hide the copybutton first
+ setTimeout(() => el.classList.remove('success'), timeoutSuccessClass)
+ setTimeout(() => el.setAttribute('data-tooltip', oldText), timeoutIcon)
+}
+
+// Changes the copy button icon for two seconds, then changes it back
+const temporarilyChangeIcon = (el) => {
+ el.innerHTML = iconCheck;
+ setTimeout(() => {el.innerHTML = iconCopy}, timeoutIcon)
+}
+
+const addCopyButtonToCodeCells = () => {
+ // If ClipboardJS hasn't loaded, wait a bit and try again. This
+ // happens because we load ClipboardJS asynchronously.
+ if (window.ClipboardJS === undefined) {
+ setTimeout(addCopyButtonToCodeCells, 250)
+ return
+ }
+
+ // Add copybuttons to all of our code cells
+ const COPYBUTTON_SELECTOR = '{{ copybutton_selector }}';
+ const codeCells = document.querySelectorAll(COPYBUTTON_SELECTOR)
+ codeCells.forEach((codeCell, index) => {
+ const id = codeCellId(index)
+ codeCell.setAttribute('id', id)
+
+ const clipboardButton = id =>
+ ``
+ codeCell.insertAdjacentHTML('afterend', clipboardButton(id))
+ })
+
+{{ copybutton_format_func }}
+
+var copyTargetText = (trigger) => {
+ var target = document.querySelector(trigger.attributes['data-clipboard-target'].value);
+
+ // get filtered text
+ let exclude = '{{ copybutton_exclude }}';
+
+ let text = filterText(target, exclude);
+ return formatCopyText(text, {{ "{!r}".format(copybutton_prompt_text) }}, {{ copybutton_prompt_is_regexp | lower }}, {{ copybutton_only_copy_prompt_lines | lower }}, {{ copybutton_remove_prompts | lower }}, {{ copybutton_copy_empty_lines | lower }}, {{ "{!r}".format(copybutton_line_continuation_character) }}, {{ "{!r}".format(copybutton_here_doc_delimiter) }})
+}
+
+ // Initialize with a callback so we can modify the text before copy
+ const clipboard = new ClipboardJS('.copybtn', {text: copyTargetText})
+
+ // Update UI with error/success messages
+ clipboard.on('success', event => {
+ clearSelection()
+ temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_success'])
+ temporarilyChangeIcon(event.trigger)
+ })
+
+ clipboard.on('error', event => {
+ temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_failure'])
+ })
+}
+
+runWhenDOMLoaded(addCopyButtonToCodeCells)
diff --git a/docs/sphinx_exts/sphinx_copybutton/_static/copybutton_funcs.js b/docs/sphinx_exts/sphinx_copybutton/_static/copybutton_funcs.js
new file mode 100644
index 000000000..dbe1aaad7
--- /dev/null
+++ b/docs/sphinx_exts/sphinx_copybutton/_static/copybutton_funcs.js
@@ -0,0 +1,73 @@
+function escapeRegExp(string) {
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
+}
+
+/**
+ * Removes excluded text from a Node.
+ *
+ * @param {Node} target Node to filter.
+ * @param {string} exclude CSS selector of nodes to exclude.
+ * @returns {DOMString} Text from `target` with text removed.
+ */
+export function filterText(target, exclude) {
+ const clone = target.cloneNode(true); // clone as to not modify the live DOM
+ if (exclude) {
+ // remove excluded nodes
+ clone.querySelectorAll(exclude).forEach(node => node.remove());
+ }
+ return clone.innerText;
+}
+
+// Callback when a copy button is clicked. Will be passed the node that was clicked
+// should then grab the text and replace pieces of text that shouldn't be used in output
+export function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") {
+ var regexp;
+ var match;
+
+ // Do we check for line continuation characters and "HERE-documents"?
+ var useLineCont = !!lineContinuationChar
+ var useHereDoc = !!hereDocDelim
+
+ // create regexp to capture prompt and remaining line
+ if (isRegexp) {
+ regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)')
+ } else {
+ regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)')
+ }
+
+ const outputLines = [];
+ var promptFound = false;
+ var gotLineCont = false;
+ var gotHereDoc = false;
+ const lineGotPrompt = [];
+ for (const line of textContent.split('\n')) {
+ match = line.match(regexp)
+ if (match || gotLineCont || gotHereDoc) {
+ promptFound = regexp.test(line)
+ lineGotPrompt.push(promptFound)
+ if (removePrompts && promptFound) {
+ outputLines.push(match[2])
+ } else {
+ outputLines.push(line)
+ }
+ gotLineCont = line.endsWith(lineContinuationChar) & useLineCont
+ if (line.includes(hereDocDelim) & useHereDoc)
+ gotHereDoc = !gotHereDoc
+ } else if (!onlyCopyPromptLines) {
+ outputLines.push(line)
+ } else if (copyEmptyLines && line.trim() === '') {
+ outputLines.push(line)
+ }
+ }
+
+ // If no lines with the prompt were found then just use original lines
+ if (lineGotPrompt.some(v => v === true)) {
+ textContent = outputLines.join('\n');
+ }
+
+ // Remove a trailing newline to avoid auto-running when pasting
+ if (textContent.endsWith("\n")) {
+ textContent = textContent.slice(0, -1)
+ }
+ return textContent
+}