Skip to content

Commit 556e499

Browse files
committed
Add regexp matching
1 parent a7e53bc commit 556e499

File tree

7 files changed

+199
-35
lines changed

7 files changed

+199
-35
lines changed

doc/conf.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
# Add any Sphinx extension module names here, as strings. They can be
3939
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
4040
# ones.
41-
extensions = ["sphinx_copybutton"]
41+
extensions = ["sphinx_copybutton", "IPython.sphinxext.ipython_directive"]
4242

4343
# Add any paths that contain templates here, relative to this directory.
4444
templates_path = ["_templates"]
@@ -98,7 +98,8 @@
9898
# html_sidebars = {}
9999

100100
# CopyButton configuration
101-
copybutton_prompt_text = ">>> "
101+
copybutton_prompt_text = ">>> |\\\\$ |\\[\\d*\\]: |\\.\\.\\.: "
102+
copybutton_prompt_is_regexp = True
102103
# Switches for testing but shouldn't be activated in the live docs
103104
# copybutton_only_copy_prompt_lines = False
104105
# copybutton_remove_prompts = False

doc/index.rst

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,34 @@ use the following configuration:
135135
136136
copybutton_prompt_text = ">>> "
137137
138+
Using regexp prompt identifiers
139+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
140+
141+
If your prompts are more complex than a single string, then you can use a regexp to match with.
142+
143+
.. code-block:: python
144+
145+
copybutton_prompt_text = "\\[\\d*\\]: |\\.\\.\\.: "
146+
copybutton_prompt_is_regexp = True
147+
148+
For example when using ipython prompts with continuations:
149+
150+
.. code-block:: restructuredtext
151+
152+
.. code-block:: ipython
153+
154+
[1]: first
155+
...: continuation
156+
output
157+
[2]: second
158+
159+
.. code-block:: ipython
160+
161+
[1]: first
162+
...: continuation
163+
output
164+
[2]: second
165+
138166
Configure whether *only* lines with prompts are copied
139167
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
140168

@@ -265,7 +293,7 @@ Then run the docs build:
265293
266294
.. code-block:: console
267295
268-
$ cd docs
296+
$ cd doc
269297
$ make html
270298
271299
.. toctree::

doc/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
sphinx
2+
ipython
23

34
# Install ourselves
45
.

sphinx_copybutton/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ def add_to_context(app, config):
1818
config.html_context.update(
1919
{"copybutton_prompt_text": config.copybutton_prompt_text}
2020
)
21+
config.html_context.update(
22+
{"copybutton_prompt_is_regexp": config.copybutton_prompt_is_regexp}
23+
)
2124
config.html_context.update(
2225
{"copybutton_only_copy_prompt_lines": config.copybutton_only_copy_prompt_lines}
2326
)
@@ -37,12 +40,14 @@ def add_to_context(app, config):
3740

3841

3942
def setup(app):
43+
4044
logger.verbose("Adding copy buttons to code blocks...")
4145
# Add our static path
4246
app.connect("builder-inited", scb_static_path)
4347

4448
# configuration for this tool
4549
app.add_config_value("copybutton_prompt_text", "", "html")
50+
app.add_config_value("copybutton_prompt_is_regexp", False, "html")
4651
app.add_config_value("copybutton_only_copy_prompt_lines", True, "html")
4752
app.add_config_value("copybutton_remove_prompts", True, "html")
4853
app.add_config_value("copybutton_image_path", "copy-button.svg", "html")

sphinx_copybutton/_static/copybutton.js_t

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,9 @@ const addCopyButtonToCodeCells = () => {
8484

8585
{{ copybutton_format_func }}
8686

87-
var copyTargetText = (target) => {
87+
var copyTargetText = (trigger) => {
8888
var target = document.querySelector(trigger.attributes['data-clipboard-target'].value);
89-
return formatCopyText(target.innerText, '{{ copybutton_prompt_text }}', {{ copybutton_only_copy_prompt_lines | lower }}, {{ copybutton_remove_prompts | lower }})
89+
return formatCopyText(target.innerText, '{{ copybutton_prompt_text }}', {{ copybutton_prompt_is_regexp | lower }}, {{ copybutton_only_copy_prompt_lines | lower }}, {{ copybutton_remove_prompts | lower }})
9090
}
9191

9292
// Initialize with a callback so we can modify the text before copy

sphinx_copybutton/_static/copybutton_funcs.js

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,44 @@
1+
function escapeRegExp(string) {
2+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
3+
}
4+
15
// Callback when a copy button is clicked. Will be passed the node that was clicked
26
// should then grab the text and replace pieces of text that shouldn't be used in output
3-
export function formatCopyText(textContent, copybuttonPromptText, onlyCopyPromptLines=true, removePrompts=true) {
4-
textContent = textContent.split('\n');
5-
// Text content line filtering based on prompts (if a prompt text is given)
6-
if (copybuttonPromptText.length > 0) {
7-
// If only copying prompt lines, remove all lines that don't start w/ prompt
8-
if (onlyCopyPromptLines) {
9-
var linesWithPrompt = textContent.filter((line) => {
10-
return line.startsWith(copybuttonPromptText) || (line.length == 0); // Keep newlines
11-
});
12-
// Check to make sure we have at least one non-empty line
13-
var nonEmptyLines = linesWithPrompt.filter((line) => { return line.length > 0 });
14-
// If we detected lines w/ prompt, then overwrite textContent w/ those lines
15-
if ((linesWithPrompt.length > 0) && (nonEmptyLines.length > 0)) {
16-
textContent = linesWithPrompt;
7+
export function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true) {
8+
9+
var regexp;
10+
var match;
11+
12+
// create regexp to capture prompt and remaining line
13+
if (isRegexp) {
14+
regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)')
15+
} else {
16+
regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)')
17+
}
18+
19+
const outputLines = [];
20+
var promptFound = false;
21+
for (const line of textContent.split('\n')) {
22+
match = line.match(regexp)
23+
if (match) {
24+
promptFound = true
25+
if (removePrompts) {
26+
outputLines.push(match[2])
27+
} else {
28+
outputLines.push(line)
29+
}
30+
} else {
31+
if (!onlyCopyPromptLines) {
32+
outputLines.push(line)
1733
}
1834
}
19-
// Remove the starting prompt from any remaining lines
20-
if (removePrompts) {
21-
textContent.forEach((line, index) => {
22-
if (line.startsWith(copybuttonPromptText)) {
23-
textContent[index] = line.slice(copybuttonPromptText.length);
24-
}
25-
});
26-
}
2735
}
28-
textContent = textContent.join('\n');
36+
37+
// If no lines with the prompt were found then just use original lines
38+
if (promptFound) {
39+
textContent = outputLines.join('\n');
40+
}
41+
2942
// Remove a trailing newline to avoid auto-running when pasting
3043
if (textContent.endsWith("\n")) {
3144
textContent = textContent.slice(0, -1)

sphinx_copybutton/_static/test.js

Lines changed: 123 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,138 @@ import { formatCopyText } from "./copybutton_funcs";
33

44
const parameters = [
55
{
6-
description: 'no prompt',
6+
description: 'empty prompt',
77
text: 'hallo',
88
prompt: '',
9+
isRegexp: false,
10+
onlyCopyPromptLines: true,
11+
removePrompts: true,
912
expected: 'hallo'
1013
},
1114
{
12-
description: 'with prompt',
13-
text: '>>> hallo',
15+
description: 'no prompt in text',
16+
text: 'hallo',
1417
prompt: '>>> ',
18+
isRegexp: false,
19+
onlyCopyPromptLines: true,
20+
removePrompts: true,
1521
expected: 'hallo'
22+
},
23+
{
24+
description: 'with non-regexp python prompt',
25+
text: `
26+
>>> first
27+
output
28+
>>> second`,
29+
prompt: '>>> ',
30+
isRegexp: false,
31+
onlyCopyPromptLines: true,
32+
removePrompts: true,
33+
expected: 'first\nsecond'
34+
},
35+
{
36+
description: 'with non-regexp console prompt',
37+
text: `
38+
$ first
39+
output
40+
$ second`,
41+
prompt: '$ ',
42+
isRegexp: false,
43+
onlyCopyPromptLines: true,
44+
removePrompts: true,
45+
expected: 'first\nsecond'
46+
},
47+
{
48+
description: 'with non-regexp prompt, keep prompt',
49+
text: `
50+
>>> first
51+
output
52+
>>> second`,
53+
prompt: '>>> ',
54+
isRegexp: false,
55+
onlyCopyPromptLines: true,
56+
removePrompts: false,
57+
expected: '>>> first\n>>> second'
58+
},
59+
{
60+
description: 'with non-regexp prompt, keep lines',
61+
text: `
62+
>>> first
63+
output
64+
>>> second`,
65+
prompt: '>>> ',
66+
isRegexp: false,
67+
onlyCopyPromptLines: false,
68+
removePrompts: true,
69+
expected: '\nfirst\noutput\nsecond'
70+
},
71+
{
72+
description: 'with non-regexp prompt, keep all',
73+
text: `
74+
>>> first
75+
output
76+
>>> second`,
77+
prompt: '>>> ',
78+
isRegexp: false,
79+
onlyCopyPromptLines: false,
80+
removePrompts: false,
81+
expected: '\n>>> first\noutput\n>>> second'
82+
},
83+
{
84+
description: 'with regexp python prompt',
85+
text: `
86+
>>> first
87+
output
88+
>>> second`,
89+
prompt: '>>> ',
90+
isRegexp: true,
91+
onlyCopyPromptLines: true,
92+
removePrompts: true,
93+
expected: 'first\nsecond'
94+
},
95+
{
96+
description: 'with regexp console prompt',
97+
text: `
98+
$ first
99+
output
100+
$ second`,
101+
prompt: '\\$ ',
102+
isRegexp: true,
103+
onlyCopyPromptLines: true,
104+
removePrompts: true,
105+
expected: 'first\nsecond'
106+
},
107+
{
108+
description: 'with ipython prompt regexp',
109+
text: `
110+
[1]: first
111+
...: continuation
112+
output
113+
[2]: second`,
114+
prompt: '[\d*]: |\.\.\.: ',
115+
isRegexp: true,
116+
onlyCopyPromptLines: true,
117+
removePrompts: true,
118+
expected: 'first\ncontinuation\nsecond'
119+
},
120+
{
121+
description: 'with ipython prompt regexp, keep prompts',
122+
text: `
123+
[1]: first
124+
...: continuation
125+
output
126+
[2]: second`,
127+
prompt: '[\d*]: |\.\.\.: ',
128+
isRegexp: true,
129+
onlyCopyPromptLines: true,
130+
removePrompts: false,
131+
expected: '[1]: first\n...: continuation\n[2]: second'
16132
}
17133
]
18134

19-
parameters.forEach((parameter) => {
20-
test(parameter.description, t => {
21-
const text = formatCopyText(parameter.text, parameter.prompt);
22-
t.is(text, parameter.expected)
135+
parameters.forEach((p) => {
136+
test(p.description, t => {
137+
const text = formatCopyText(p.text, p.prompt, p.isRegexp, p.onlyCopyPromptLines, p.removePrompts);
138+
t.is(text, p.expected)
23139
});
24140
})

0 commit comments

Comments
 (0)