Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exclude unselectable text from being copied #138

Closed
wants to merge 14 commits into from

Conversation

bicarlsen
Copy link
Contributor

In the sphinx_rtd_theme I was using the :linenos: option for the literalinclude directive to display line numbers for my code blocks. However, when using the copybutton extenstion for users to easily copy the code, the line numbers were being included in the copied text, even though they aren't selectable by the user.

I added functionality to exclude this unselectabel text from the copied text, along with an option copybutton_exclude_unselectable to determine its behavior. This option is by default true.

…electable text should be excluded from copy and do so if needed. [_static/copybutton_funcs.js] Added functionality to get unselectable elements and remove their text.
@welcome
Copy link

welcome bot commented Sep 20, 2021

Thanks for submitting your first pull request! You are awesome! 🤗

If you haven't done so already, check out EBP's Code of Conduct and our Contributing Guide, as this will greatly help the review process.

Welcome to the EBP community! 🎉

@bicarlsen
Copy link
Contributor Author

When running pre-commint run --all and npm test on my local machine all the tests pass, so I am not sure why it is failing here.

@theletterf
Copy link

This should be merged, @choldgraf. Could you give it a look?

docs/index.rst Outdated Show resolved Hide resolved
Copy link
Member

@choldgraf choldgraf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hey @bicarlsen - thanks for this PR! I have a few comments and requests in there, mostly around making sure that this addition is documented clearly so that others can understand it and maintain it in the future.

One other quick question - can we imagine any situation where an author would want a non-selectable text to be copied? If not then I'd be fine just making this happen and not exposing another config key

@@ -118,7 +118,11 @@ const addCopyButtonToCodeCells = () => {

var copyTargetText = (trigger) => {
var target = document.querySelector(trigger.attributes['data-clipboard-target'].value);
return formatCopyText(target.innerText, {{ "{!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) }})
let text = (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you provide a comment about what this is doing? This uses some JS syntax that I haven't seen before (with ? and :) and we want this theme to be maintainable for non-JS developers so it'd help to have extra explanation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The p ? t : f syntax is the ternary operator that returns t if p is true and f otherwise.

@@ -2,10 +2,54 @@ function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}

function getUnselectable(target) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please provide a high-level docstring that describes what this function does

return unselectable;
}

function getFilteredText(target, exclude) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here - please provide a docstring

docs/index.rst Outdated
Exclude unselectable text from copied text
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

By default, ``sphinx-copybutton`` will exclude text that is not selectable by the user. Unselectable text is determined with the CSS style
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
By default, ``sphinx-copybutton`` will exclude text that is not selectable by the user. Unselectable text is determined with the CSS style
By default, ``sphinx-copybutton`` will exclude text that is not selectable by the user.
Unselectable text is determined with the CSS style
Suggested change
By default, ``sphinx-copybutton`` will exclude text that is not selectable by the user. Unselectable text is determined with the CSS style
By default, ``sphinx-copybutton`` will exclude text that is not selectable by the user. Unselectable text is determined with the CSS style

docs/index.rst Outdated
Exclude unselectable text from copied text
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

By default, ``sphinx-copybutton`` will exclude text that is not selectable by the user. Unselectable text is determined with the CSS style
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you provide an example of what un-selectable text generally is, so that users understand when they'll likely use this?

@choldgraf
Copy link
Member

choldgraf commented Oct 11, 2021

One more quick thought - another option would be to let users specify one or more classes that should be removed from the text before copying. That way I think we could simplify the copying logic considerably. For example, if the default class to be excluded was .linenos, then the code could simply be:

// Clone our target so we don't modify the live DOM
clone = target.cloneNode(true)

// Select the classes we want to remove, and then remove each one
clone.querySelectorAll(".linenos").forEach((node) => (node.remove()))

// This should not have any elements we don't want
textToCopy = clone.innerText;

I've just merged a PR to add a couple minimal examples to the docs that should demonstrate this behavior not working. We can use this to confirm that this PR does, in fact, fix the problem :-)

#140

@bicarlsen
Copy link
Contributor Author

can we imagine any situation where an author would want a non-selectable text to be copied? If not then I'd be fine just making this happen and not exposing another config key

I can not, but figured I would leave it as an option just in case. I'm happy to remove it though.

@bicarlsen
Copy link
Contributor Author

One more quick thought - another option would be to let users specify one or more classes that should be removed from the text before copying. That way I think we could simplify the copying logic considerably.

I agree and I mentioned this in regard to Issue #9 as I think it is all tied in together. I'm happy to add that functionality, but wanted to get commetns on it first.

@choldgraf
Copy link
Member

I do like the simplicity of the implementation in #138 (comment) so unless we can see any downsides to that approach, I'd be fine going that direction

@bicarlsen
Copy link
Contributor Author

@choldgraf , I agree about your comment regarding #138 (comment). There is a reason I did it recursively, but can't remember the exact reason now. I'll reimplmenet using the Node.clone method, and keep the recursive method off to the side incase it's needed later.

@bicarlsen
Copy link
Contributor Author

I remember why I did it recursively. It's because I exclude Node's who have the userSelect style set to none. However, this property is only available when using getComputedStyle, which must be used on each Node individually.

@choldgraf , what is your opinion on excluding unselectable Nodes via th .linenos class, or by checking directly the userSelect property?

@choldgraf
Copy link
Member

@bicarlsen sorry for the slow response! My general tendency is to go with the simpler possible where we can, unless there's a strong reason to use the more complex approach. Here, it seems like excluding the .linenos class will get us roughly the same functionality with a lot less JS, so I'd be +1 on taking that approach

@bicarlsen
Copy link
Contributor Author

bicarlsen commented Oct 25, 2021

@choldgraf Great, we should be good to go. I haven't written any tests for it yet, but will do so as soon as I have time.

@bicarlsen
Copy link
Contributor Author

@choldgraf just wanted to give this a bump, since it's been a while.

Copy link
Member

@choldgraf choldgraf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a quick comment about the config, but this looks pretty good to me.

you mentioned that you wanted to write tests, I think that still needs to happen right?


.. code-block:: python

copybutton_exclude = '.exclude_me'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about instead of hard-coding .linenos, we just set this value to be a list that defaults to ['.linenos']. Then if somebody wants to define their own exclusion list, they can provide it and include .linenos as well on their own. This seems a bit simpler

docs/index.rst Outdated
@@ -395,6 +404,7 @@ For example, to make the copy button visible by default (not just when a code ce
html_css_files = ["custom.css"]

See the `Sphinx documentation on custom CSS for more information <https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-html_static_path>`_.
>>>>>>> 6ee971ab8f9c3741d380240c180b1ff47f1d2ddc
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you have some git merge resolution cruft in here

…ith funcitons using .innerText. [package.json] Added browser-env module for DOM support in testing. [docs/index] Updated documentation. [copybutton, copybutton_funcs] Fixed minor errors. Updated default exclude text. [__init__] Updated default exclude text.
@bicarlsen
Copy link
Contributor Author

you mentioned that you wanted to write tests, I think that still needs to happen right?

I tried to implement testing but apparently it is diffuclt to test functions that use .innerText because it requires a layout engine, which jsdom, which most of the testing utilities use, doesn't include.

I've included the tests in the test.js file, but commented out their actual running.

@rkdarst
Copy link
Collaborator

rkdarst commented Mar 22, 2022

I just found a reason why "unselectable text" is better than .linenos:

When rendering shell prompts, the $ shell prompt before a command is un-selectable, so that copying does the right thing when copying. Using the copybutton currently includes $. Who knows what other highlighters would be this smart.

Example: text boxes found in this section: https://coderefinery.org/git-intro/basics/#recording-a-snapshot-with-git

So, my vote for the "unselectable text" method (not that it counts for much).

@rkdarst
Copy link
Collaborator

rkdarst commented Jun 10, 2022

This became an issue in a workshop, so I did more investigation.
The previous version of this PR (minimum working copy: https://github.com/rkdarst/sphinx-copybutton/tree/exclude-unselectable-new ) did work to exclude unselectable text, including the $ in shell-session highlighted blocks.

But, in my live site, even normal un-selection didn't work. It turns out that not selecting $ comes from sphinx_rtd_theme, which is interesting, since the parsing comes from pygments (and the reason it doesn't work on my live site on readthedocs is that it uses sphinx_rtd_theme that is too old):
https://github.com/readthedocs/sphinx_rtd_theme/blob/0da22b885be387b79f7552c92be00fd14d11228a/src/sass/_theme_rst.sass#L103-L106

  div.highlight
    span.linenos, .gp
      user-select: none
      pointer-events: none

If you search pygments, I find that gp stands for the Generic.Prompt token, so there is already some sort of abstraction layer leakage between the themes and parsers.

So my proposal:

  • decide if we go with unselectable text or configurable.
    • Now I am more on the side of the current strategy (explicitly define the excluded classes), but the .lineno .gp information will start being copied around more and more places which doesn't seem ideal, but anyway it's still going to be copied to the themes...
  • add .gp to the list of default excludes if we stay with this strategy
  • add .gp as user-select: none to sphinx-book-theme? a quick grep doesn't show that string in the relevant context. I don't even know if it uses pygments as a highlighter so that this is relevant, though. (I don't yet use the theme myself)

Copy link
Collaborator

@rkdarst rkdarst left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trying to address one of the requests from choldgraf.

And proposal of adding .gp to the default exclude list, but I'm not yet sure if that is a good idea.

Exclude text
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

You may exclude elements matching CSS selectors from being copied by specifying the `copybutton_exclude` option in ``conf.py``.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
You may exclude elements matching CSS selectors from being copied by specifying the `copybutton_exclude` option in ``conf.py``.
You may exclude elements matching CSS selectors from being copied by specifying the `copybutton_exclude` option in ``conf.py``.
For example, a viewer usually doesn't want to copy the line numbers, and CSS provides a way to exclude this. This option
implements that option for sphinx-copybutton as well.

@@ -372,6 +372,17 @@ In this case, all elements that match ``your.selector`` will have a copy button
added to them.


Exclude text
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Exclude text
Exclude text from being copied


copybutton_exclude = '.exclude_me'

By default `.linenos` is excluded. If you specify the `copybutton_exclude` option it will replace the default.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
By default `.linenos` is excluded. If you specify the `copybutton_exclude` option it will replace the default.
By default `.linenos, .gp` are excluded. If you specify the `copybutton_exclude` option it will replace the default.
`.linenos` is the Sphinx default for line numbers, and `.gp` is Pygments (the highlighter of Sphinx) for "prompt", e.g. `$` in a shell-session.

I am unsure if we want .gp by default, but since sphinx_rtd_theme (and probably others should as well) makes it unselectable by default, it seems reasonable that we mirror that behavior.

But, if we add .gp by default, then themes which don't un-select .gp will confuse users. If we do add it, users in themes that understand pygments have to change things. Hm, seeming more like like interpreting the CSS has some sort of advantages but I'm not sure if it's worth the extra work yet...

@@ -68,6 +69,7 @@ def setup(app):
app.add_config_value("copybutton_here_doc_delimiter", "", "html")
app.add_config_value("copybutton_image_path", "copy-button.svg", "html")
app.add_config_value("copybutton_selector", "div.highlight pre", "html")
app.add_config_value("copybutton_exclude", ".linenos", "html")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
app.add_config_value("copybutton_exclude", ".linenos", "html")
app.add_config_value("copybutton_exclude", ".linenos, .gp", "html")

Wait, does this work to select multiple? In the end it gets passed to the filterText function.

@rkdarst
Copy link
Collaborator

rkdarst commented Jun 16, 2022

For reference, I just found this:

Apparently .gp (GenericPrompt) is also now excluded in the basic Sphinx CSS:
https://github.com/sphinx-doc/sphinx/pull/9120/files (a bit more commentary on this)

Sphinx basic.css_t excludes the following things: table.highlighttable td.linenos, span.linenos, div.highlight span.gp.

Upstream pygments doesn't seem to do any user-select: none in any CSS (though I'm not sure if it is designed to do this, there aren't many *.css* files in there)

rkdarst added a commit to rkdarst/sphinx-copybutton that referenced this pull request Sep 13, 2022
- `.linenos` is the line numbers, and `.gp` is the tag from the
  pygments highlighter for things like the `$` prompt.  Upstream
  sphinx CSS excludes `.gp` from being copied, see the analysis here:

  executablebooks#138 (comment)

- If upstream sphinx excludes `.gp`, we probably should as well.
rkdarst added a commit to rkdarst/sphinx-copybutton that referenced this pull request Sep 13, 2022
- Re-add documentation from PR executablebooks#138 - that PR got conflicted with a
  docs rearrangement, so I add the docs back in one chunk now.  I
  include my own suggestions in that PR.
@rkdarst
Copy link
Collaborator

rkdarst commented Sep 13, 2022

#178 is a rebase of this that resolves conflicts with the docs and hopefully includes the suggestions from above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants