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

✨ NEW: Add line continuation matching and "HERE-documents" #126

Merged
merged 20 commits into from
Jun 11, 2021
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@
# copybutton_remove_prompts = False
# copybutton_image_path = "test/TEST_COPYBUTTON.png"
# copybutton_selector = "div"
copybutton_end_of_line_character = "\\"
sappelhoff marked this conversation as resolved.
Show resolved Hide resolved
copybutton_here_doc_delimiter = "EOT"


# -- Options for HTMLHelp output ---------------------------------------------
Expand Down
54 changes: 54 additions & 0 deletions doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,60 @@ To disable copying empty lines, use the following configuration in ``conf.py``:

copybutton_copy_empty_lines = False

Configure whether to copy across end of line characters
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
sappelhoff marked this conversation as resolved.
Show resolved Hide resolved

Sometimes you may wish to copy a code block like this one:

.. code-block:: bash

$ datalad download-url http://www.tldp.org/LDP/Bash-Beginners-Guide/Bash-Beginners-Guide.pdf \
--dataset . \
-m "add beginners guide on bash" \
-O books/bash_guide.pdf

Assuming that you specified ``$`` as your prompt, sphinx-copybutton will only copy
the first line by default.

To copy all lines above, you can use the following configuration:

.. code-block:: python

copybutton_end_of_line_character = "\\"
sappelhoff marked this conversation as resolved.
Show resolved Hide resolved

Configure whether to copy across end of line characters
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
sappelhoff marked this conversation as resolved.
Show resolved Hide resolved

`Here Documents <https://en.wikipedia.org/wiki/Here_document>`_ are a form of multiline string literals
in which line breaks and other whitespace (including indentation) is preserved.
For example:

.. code-block:: bash

$ cat << EOT > notes.txt
This is an example sentence.
Put some indentation on this line.

EOT

Executing this codeblock in the terminal will create a file ``notes.txt`` with the exact
text from line two of the codeblock until (but not including) the final line containing ``EOT``.

Howver, assuming that you specified ``$`` as your prompt, sphinx-copybutton will only copy
the first line by default.

sphinx-copybutton can be configured to copy the whole "here document" by using the following
configuration:

.. code-block:: python

copybutton_here_doc_delimiter = "EOT"

This will continue to look for lines to copy based on the rules above,
but if one of the lines to be copied contains the defined delimiter (here: ``EOT``),
then all following lines will be copied literally until the next delimiter is
encountered in a line.

Use a different copy button image
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
8 changes: 8 additions & 0 deletions sphinx_copybutton/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ def add_to_context(app, config):
config.html_context.update(
{"copybutton_copy_empty_lines": config.copybutton_copy_empty_lines}
)
config.html_context.update(
{"copybutton_end_of_line_character": config.copybutton_end_of_line_character}
sappelhoff marked this conversation as resolved.
Show resolved Hide resolved
)
config.html_context.update(
{"copybutton_here_doc_delimiter": config.copybutton_here_doc_delimiter}
)
config.html_context.update({"copybutton_image_path": config.copybutton_image_path})
config.html_context.update({"copybutton_selector": config.copybutton_selector})
config.html_context.update(
Expand All @@ -54,6 +60,8 @@ def setup(app):
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_end_of_line_character", "", "html")
sappelhoff marked this conversation as resolved.
Show resolved Hide resolved
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")

Expand Down
2 changes: 1 addition & 1 deletion sphinx_copybutton/_static/copybutton.js_t
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ 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 }})
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_end_of_line_character) }}, {{ "{!r}".format(copybutton_here_doc_delimiter) }})
sappelhoff marked this conversation as resolved.
Show resolved Hide resolved
}

// Initialize with a callback so we can modify the text before copy
Expand Down
31 changes: 26 additions & 5 deletions sphinx_copybutton/_static/copybutton_funcs.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,25 @@ function escapeRegExp(string) {

// 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) {
export function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, endOfLineChar = "", hereDocDelim = "") {

var regexp;
var match;

// Do we check for end of line characters and "here documents"?
var useEndOfLine = true
if (!endOfLineChar) {
endOfLineChar = ''
useEndOfLine = false
}

var useHereDoc = true
if (!hereDocDelim) {
hereDocDelim = ''
useHereDoc = false
}


// create regexp to capture prompt and remaining line
if (isRegexp) {
regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)')
Expand All @@ -18,15 +32,22 @@ export function formatCopyText(textContent, copybuttonPromptText, isRegexp = fal

const outputLines = [];
var promptFound = false;
var gotEndOfLine = false;
var gotHereDoc = false;
const lineGotPrompt = [];
for (const line of textContent.split('\n')) {
match = line.match(regexp)
if (match) {
promptFound = true
if (removePrompts) {
if (match || gotEndOfLine || gotHereDoc) {
promptFound = regexp.test(line)
lineGotPrompt.push(promptFound)
if (removePrompts && promptFound) {
outputLines.push(match[2])
} else {
outputLines.push(line)
}
gotEndOfLine = line.endsWith(endOfLineChar) & useEndOfLine
if (line.includes(hereDocDelim) & useHereDoc)
gotHereDoc = !gotHereDoc
} else if (!onlyCopyPromptLines) {
outputLines.push(line)
} else if (copyEmptyLines && line.trim() === '') {
Expand All @@ -35,7 +56,7 @@ export function formatCopyText(textContent, copybuttonPromptText, isRegexp = fal
}

// If no lines with the prompt were found then just use original lines
if (promptFound) {
if (lineGotPrompt.some(v => v === true)) {
textContent = outputLines.join('\n');
}

Expand Down
52 changes: 51 additions & 1 deletion sphinx_copybutton/_static/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,56 @@ output
removePrompts: false,
expected: '\n>>> first\n>>> second'
},
{
description: 'multiline with |, keep prompt',
text: `
>>> first |
output |
is |
fine
is it?
>>> second`,
prompt: '>>> ',
isRegexp: false,
onlyCopyPromptLines: true,
removePrompts: false,
copyEmptyLines: false,
endOfLineChar: '|',
expected: '>>> first |\noutput |\nis |\nfine\n>>> second'
},
{
description: 'multiline with \\, remove prompt',
text: `
$ datalad download-url http://www.tldp.org/LDP/Bash-Beginners-Guide/Bash-Beginners-Guide.pdf \\
--dataset . \\
-m "add beginners guide on bash" \\
-O books/bash_guide.pdf
`,
prompt: '$ ',
isRegexp: false,
onlyCopyPromptLines: true,
removePrompts: true,
copyEmptyLines: false,
endOfLineChar: '\\',
expected: 'datalad download-url http://www.tldp.org/LDP/Bash-Beginners-Guide/Bash-Beginners-Guide.pdf \\\n--dataset . \\\n-m "add beginners guide on bash" \\\n-O books/bash_guide.pdf'
},
{
description: 'multiline with "here document", remove prompt',
text: `
$ cat << EOT > notes.txt
One can hear a joke.
And laugh.

EOT
`,
prompt: '$ ',
isRegexp: false,
onlyCopyPromptLines: true,
removePrompts: true,
copyEmptyLines: false,
hereDocDelim: "EOT",
expected: 'cat << EOT > notes.txt\nOne can hear a joke.\nAnd laugh.\n\nEOT'
},
{
description: 'with non-regexp prompt, keep lines',
text: `
Expand Down Expand Up @@ -181,7 +231,7 @@ output

parameters.forEach((p) => {
test(p.description, t => {
const text = formatCopyText(p.text, p.prompt, p.isRegexp, p.onlyCopyPromptLines, p.removePrompts, p.copyEmptyLines);
const text = formatCopyText(p.text, p.prompt, p.isRegexp, p.onlyCopyPromptLines, p.removePrompts, p.copyEmptyLines, p.endOfLineChar, p.hereDocDelim);
t.is(text, p.expected)
});
})