diff --git a/.github/workflows/artifacts.yml b/.github/workflows/artifacts.yml new file mode 100644 index 0000000..cf5a036 --- /dev/null +++ b/.github/workflows/artifacts.yml @@ -0,0 +1,12 @@ +on: [status] + +jobs: + circleci_artifacts_redirector_job: + runs-on: ubuntu-latest + name: Run CircleCI artifacts redirector + steps: + - name: GitHub Action step + uses: larsoner/circleci-artifacts-redirector-action@master + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + artifact-path: 0/html/index.html diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml new file mode 100644 index 0000000..6db8b40 --- /dev/null +++ b/.github/workflows/integration.yml @@ -0,0 +1,54 @@ +name: continuous-integration + +on: [push, pull_request] + +jobs: + + docs: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.6, 3.7, 3.8] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + git submodule update --init + pip install -e . + pip install -r doc/requirements.txt + + - name: Build docs + run: | + cd doc + make html + + publish: + + name: Publish to PyPi + needs: [docs] + if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') + runs-on: ubuntu-latest + steps: + - name: Checkout source + uses: actions/checkout@v2 + - name: Set up Python 3.7 + uses: actions/setup-python@v1 + with: + python-version: 3.7 + - name: Build package + run: | + pip install wheel + git submodule update --init + python setup.py sdist bdist_wheel + - name: Publish + uses: pypa/gh-action-pypi-publish@v1.1.0 + with: + user: __token__ + password: ${{ secrets.PYPI_KEY }} diff --git a/.gitignore b/.gitignore index 97dd07a..8045a79 100644 --- a/.gitignore +++ b/.gitignore @@ -105,3 +105,6 @@ venv.bak/ # Docs _build/ + +# VS code config +.vscode diff --git a/MANIFEST.in b/MANIFEST.in index 443bb8b..e5583af 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,2 @@ graft doc/ +prune doc/_build diff --git a/RELEASES.md b/RELEASES.md index a978c2f..9242cbb 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -11,19 +11,19 @@ To create a new release of Sphinx-Copybutton, you need to do these things: ## To create the release -To create a new release, [open an issue](https://github.com/choldgraf/sphinx-copybutton/issues/new) to keep +To create a new release, [open an issue](https://github.com/ExecutableBookProject/sphinx-copybutton/issues/new) to keep track of the to-do list for the release. Copy/paste the following markdown into the issue and check off the boxes as you complete items: ``` -- [ ] Ensure that the [Sphinx-Copybutton version number](https://github.com/choldgraf/sphinx-copybutton/blob/master/jupyter_book/__init__.py) +- [ ] Ensure that the [Sphinx-Copybutton version number](https://github.com/ExecutableBookProject/sphinx-copybutton/blob/master/jupyter_book/__init__.py) is correct, and remove the `dev0` part of the version number. Make a PR with the new number and merge into master. - [ ] Create a new distribution for Sphinx-Copybutton by [following the twine release instructions](https://twine.readthedocs.io/en/latest/#using-twine) - [ ] Confirm that the new version of Sphinx-Copybutton [is posted to pypi](https://pypi.org/project/sphinx-copybutton/) -- [ ] Bump the [Sphinx-Copybutton version number](https://github.com/choldgraf/sphinx-copybutton/blob/master/jupyter_book/__init__.py) to +- [ ] Bump the [Sphinx-Copybutton version number](https://github.com/ExecutableBookProject/sphinx-copybutton/blob/master/jupyter_book/__init__.py) to the next minor (or major) release and append `dev0` to the end. - [ ] Celebrate! You've just released a new version of Sphinx-Copybutton! ``` diff --git a/doc/_static/test/TEST_COPYBUTTON.png b/doc/_static/test/TEST_COPYBUTTON.png new file mode 100644 index 0000000..2787393 Binary files /dev/null and b/doc/_static/test/TEST_COPYBUTTON.png differ diff --git a/doc/conf.py b/doc/conf.py index 75d20c3..1cafea7 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -97,6 +97,14 @@ # # html_sidebars = {} +# CopyButton configuration +copybutton_prompt_text = ">>> " +# Switches for testing but shouldn't be activated in the live docs +# copybutton_only_copy_prompt_lines = False +# copybutton_remove_prompts = False +# copybutton_image_path = "test/TEST_COPYBUTTON.png" +# copybutton_selector = "div" + # -- Options for HTMLHelp output --------------------------------------------- diff --git a/doc/index.rst b/doc/index.rst index e43d0a6..0089596 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -10,10 +10,10 @@ Sphinx-copybutton :target: https://pypi.org/project/sphinx_copybutton :alt: PyPi page -Sphinx-copybutton does one thing: add little "copy" button to the right +Sphinx-copybutton does one thing: add a little "copy" button to the right of your code blocks. That's it! It is a lightweight wrapper around the excellent (and also lightweight) Javascript library -`ClipboardJS `. +`ClipboardJS `_. **Here's an example** @@ -26,17 +26,6 @@ And here's a code block, note the copy button to the right! copy me! -By default, ``sphinx-copybutton`` will remove Python prompts from -each line that begins with them. For example, try copying the text -below: - -.. code-block:: python - - >>> a = 2 - >>> print(a) - -The text that ``sphinx-copybutton`` uses can be configured as well. See -:ref:`configure_copy_text` for more information. If the code block overlaps to the right of the text area, you can just click the button to get the whole thing. @@ -45,6 +34,23 @@ the button to get the whole thing. 123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789 +You can configure ``sphinx-copybutton`` to detect *input prompts* in code +cells, and then both remove these prompts before copying, as well as skip +lines that *don't* start with prompts (in case they are output lines). + +For example, this site has been configured to strip Python prompts (">>> "). +Try copy-pasting the code block below. + +.. code-block:: python + + >>> a = 2 + >>> print(a) + 2 + + >>> b = 'wow' + >>> print(b) + wow + Installation ============ @@ -54,7 +60,7 @@ You can install ``sphinx-copybutton`` with ``pip``: pip install sphinx-copybutton -`Here's a link to the sphinx-copybutton GitHub repository `_. +`Here's a link to the sphinx-copybutton GitHub repository `_. Usage ===== @@ -70,8 +76,8 @@ extensions list. E.g.: ... ] -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! +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 ============= @@ -86,58 +92,105 @@ Customize the CSS To customize the display of the copy button, you can add your own CSS files that overwrite the CSS in the -`sphinx-copybutton CSS rules `_. +`sphinx-copybutton CSS rules `_. Just add these files to ``_static`` in your documentation folder, and it should overwrite sphinx-copybutton's behavior. .. _configure_copy_text: -Customize the text that is removed during copying -------------------------------------------------- +Strip and configure input prompts for code cells +------------------------------------------------ + +By default, ``sphinx-copybutton`` will copy the entire contents of a code +block when the button is clicked. For many languages, it is common to +include **input prompts** with your examples, along with the outputs from +running the code. + +``sphinx-copybutton`` provides functionality to both +strip input prompts, as well as *only* select lines that begin with a prompt. +This allows users to click the button and *only* copy the input text, +excluding the prompts and outputs. + +To define the prompt text that you'd like removed from copied text in your code +blocks, use the following configuration value in your ``conf.py`` file: + +.. code-block:: python + + copybutton_prompt_text = "myinputprompt" + +When this variable is set, ``sphinx-copybutton`` will remove the prompt from +the beginning of any lines that start with the text you specify. In +addition, *only* the lines that contain prompts will be copied if any are +discovered. If no lines with prompts are found, then the full contents of +the cell will be copied. + +For example, to exclude traditional Python prompts from your copied code, +use the following configuration: + +.. code-block:: python + + copybutton_prompt_text = ">>> " + +Configure whether *only* lines with prompts are copied +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, if sphinx-copybutton detects lines that begin with code prompts, +it will *only* copy the text in those lines (after stripping the prompts). +This assumes that the rest of the code block contains outputs that shouldn't +be copied. -By default, ``sphinx-copybutton`` will remove Python prompts (">>> ") from -the beginning of each line. To change the text that is removed (or to remove -no text at all), add the following configuration to your ``conf.py`` file: +To disable this behavior, use the following configuration in ``conf.py``: -.. code:: python +.. code-block:: python + + copybutton_only_copy_prompt_lines = False + +In this case, all lines of the code blocks will be copied after the prompts +are stripped. - copybutton_skip_text = "sometexttoskip" +Configure whether the input prompts should be stripped +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Note that this text will only be removed from lines that *begin* with the text. +By default, sphinx-copybutton will remove the prompt text from lines +according to the value of ``copybutton_prompt_text``. + +To disable this behavior and copy the full text of lines with prompts +(for example, if you'd like to copy *only* the lines with prompts, but not +strip the prompts), use the following configuration in ``conf.py``: + +.. code-block:: python + + copybutton_remove_prompts = False Use a different copy button image ---------------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -To use a different image for your copy buttons, the easiest thing to do is -to add a small bit of javascript to your Sphinx build that points the image -to something new. Follow these steps: +To use a different image for your copy buttons, do the following: -1. Create a new javascript file in your site's static folder (e.g., `_static/js/custom.js`). - In it, put the following code: +1. Place the image in the ``_static/`` folder of your site. +2. Set the ``copybutton_image_path`` variable in your ``conf.py`` to be the + path to your image file, **relative to** ``_static/``. - .. code-block:: javascript - const updateCopyButtonImages = () => { - const copybuttonimages = document.querySelectorAll('a.copybtn img') - copybuttonimages.forEach((img, index) => { - img.setAttribute('src', 'path-to-new-image.svg') - }) - } +Configure the CSS selector used to add copy buttons +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - runWhenDOMLoaded(updateCopyButtonImages) +By default, ``sphinx-copybutton`` will add a copy button to all elements +that match the following selection: +.. code-block:: css -2. Add this javascript file to your `conf.py` configuration like so: + div.highlight pre - .. code-block:: python +To change this selector, use the following configuration in ``conf.py``: + +.. code-block:: python - def setup(app): - app.add_javascript('js/custom.js'); + copybutton_selector = "your.selector" -This will replace the copybutton images each time the page loads! +In this case, all elements that match ``your.selector`` will have a copy button +added to them. -**If you know of a better way to do this with sphinx, please don't hesitate to -recommend something!** Development =========== @@ -145,7 +198,7 @@ Development If you'd like to develop or make contributions for sphinx-copybutton, fork the repository here: -https://github.com/choldgraf/sphinx-copybutton +https://github.com/ExecutableBookProject/sphinx-copybutton pull to your computer and install locally with ``pip``:: diff --git a/setup.py b/setup.py index 8dcf3db..b6a973f 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ import os +from pathlib import Path from setuptools import setup, find_packages -from sphinx_copybutton import __version__ if (os.path.isdir('clipboard.js') and not os.path.islink('sphinx_copybutton/_static/clipboard.min.js')): @@ -18,21 +18,29 @@ with open('./README.md', 'r') as ff: readme_text = ff.read() +# Parse version +init = Path(__file__).parent.joinpath("sphinx_copybutton", "__init__.py") +for line in init.read_text().split("\n"): + if line.startswith("__version__ ="): + break +version = line.split(" = ")[-1].strip('"') + setup( name='sphinx-copybutton', - version=__version__, + version=version, description="Add a copy button to each of your code cells.", long_description=readme_text, long_description_content_type='text/markdown', - author='Chris Holdgraf', - author_email='choldgraf@berkeley.edu', - url="https://github.com/choldgraf/sphinx-copybutton", + author='Executable Book Project', + url="https://github.com/ExecutableBookProject/sphinx-copybutton", license='MIT License', packages=find_packages(), package_data={'sphinx_copybutton': ['_static/copybutton.css', - '_static/copybutton.js', + '_static/copybutton.js_t', '_static/copy-button.svg', '_static/clipboard.min.js']}, - install_requires=["flit", "setuptools", "wheel", "sphinx"], - classifiers=["License :: OSI Approved :: MIT License"] + classifiers=["License :: OSI Approved :: MIT License"], + install_requires=[ + "sphinx>=1.8" + ] ) diff --git a/sphinx_copybutton/__init__.py b/sphinx_copybutton/__init__.py index a1b0edd..af10a84 100644 --- a/sphinx_copybutton/__init__.py +++ b/sphinx_copybutton/__init__.py @@ -1,27 +1,40 @@ """A small sphinx extension to add "copy" buttons to code blocks.""" import os +from sphinx.util import logging -__version__ = "0.2.9dev0" +__version__ = "0.2.11" + +logger = logging.getLogger(__name__) def scb_static_path(app): static_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '_static')) app.config.html_static_path.append(static_path) -def add_skip_text_js(app): - skip_text = app.config['copybutton_skip_text'] - app.add_js_file(None, body="var copybuttonSkipText = '{}';".format(skip_text)) +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_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_image_path': config.copybutton_image_path}) + config.html_context.update({'copybutton_selector': config.copybutton_selector}) def setup(app): - print('Adding copy buttons to code blocks...') + logger.verbose('Adding copy buttons to code blocks...') # Add our static path app.connect('builder-inited', scb_static_path) - app.connect('builder-inited', add_skip_text_js) # configuration for this tool - app.add_config_value("copybutton_skip_text", ">>> ", "html") + app.add_config_value("copybutton_prompt_text", "", "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_image_path", "copy-button.svg", "html") + app.add_config_value("copybutton_selector", "div.highlight pre", "html") + + # Add configuration value to the template + app.connect("config-inited", add_to_context) # Add relevant code to headers - app.add_stylesheet('copybutton.css') + app.add_css_file('copybutton.css') app.add_js_file('clipboard.min.js') app.add_js_file("copybutton.js") return {"version": __version__, diff --git a/sphinx_copybutton/_static/copybutton.css b/sphinx_copybutton/_static/copybutton.css index eb70931..75b17a8 100644 --- a/sphinx_copybutton/_static/copybutton.css +++ b/sphinx_copybutton/_static/copybutton.css @@ -6,7 +6,9 @@ a.copybtn { width: 1em; height: 1em; opacity: .3; - transition: opacity 0.5s; + transition: opacity 0.5s; + border: none; + user-select: none; } div.highlight { @@ -15,6 +17,10 @@ div.highlight { a.copybtn > img { vertical-align: top; + margin: 0; + top: 0; + left: 0; + position: absolute; } .highlight:hover .copybtn { diff --git a/sphinx_copybutton/_static/copybutton.js b/sphinx_copybutton/_static/copybutton.js_t similarity index 64% rename from sphinx_copybutton/_static/copybutton.js rename to sphinx_copybutton/_static/copybutton.js_t index a5a316a..1c1757d 100644 --- a/sphinx_copybutton/_static/copybutton.js +++ b/sphinx_copybutton/_static/copybutton.js_t @@ -64,13 +64,40 @@ const temporarilyChangeTooltip = (el, newText) => { // should then grab the text and replace pieces of text that shouldn't be used in output var copyTargetText = (trigger) => { var target = document.querySelector(trigger.attributes['data-clipboard-target'].value); - var textContent = target.textContent.split('\n'); - textContent.forEach((line, index) => { - if (line.startsWith(copybuttonSkipText)) { - textContent[index] = line.slice(copybuttonSkipText.length) + var textContent = target.innerText.split('\n'); + var copybuttonPromptText = '{{ copybutton_prompt_text }}'; // Inserted from config + var onlyCopyPromptLines = {{ copybutton_only_copy_prompt_lines | lower }}; // Inserted from config + var removePrompts = {{ copybutton_remove_prompts | lower }}; // Inserted from config + + // Text content line filtering based on prompts (if a prompt text is given) + if (copybuttonPromptText.length > 0) { + // If only copying prompt lines, remove all lines that don't start w/ prompt + if (onlyCopyPromptLines) { + linesWithPrompt = textContent.filter((line) => { + return line.startsWith(copybuttonPromptText) || (line.length == 0); // Keep newlines + }); + // Check to make sure we have at least one non-empty line + var nonEmptyLines = linesWithPrompt.filter((line) => {return line.length > 0}); + // If we detected lines w/ prompt, then overwrite textContent w/ those lines + if ((linesWithPrompt.length > 0) && (nonEmptyLines.length > 0)) { + textContent = linesWithPrompt; + } + } + // Remove the starting prompt from any remaining lines + if (removePrompts) { + textContent.forEach((line, index) => { + if (line.startsWith(copybuttonPromptText)) { + textContent[index] = line.slice(copybuttonPromptText.length); + } + }); } - }); - return textContent.join('\n') + } + textContent = textContent.join('\n'); + // Remove a trailing newline to avoid auto-running when pasting + if (textContent.endsWith("\n")) { + textContent = textContent.slice(0, -1) + } + return textContent } const addCopyButtonToCodeCells = () => { @@ -82,7 +109,7 @@ const addCopyButtonToCodeCells = () => { } // Add copybuttons to all of our code cells - const codeCells = document.querySelectorAll('div.highlight pre') + const codeCells = document.querySelectorAll('{{ copybutton_selector }}') codeCells.forEach((codeCell, index) => { const id = codeCellId(index) codeCell.setAttribute('id', id) @@ -90,7 +117,7 @@ const addCopyButtonToCodeCells = () => { const clipboardButton = id => ` - ${messages[locale]['copy_to_clipboard']} + ${messages[locale]['copy_to_clipboard']} ` codeCell.insertAdjacentHTML('afterend', clipboardButton(id)) })