Skip to content

Commit

Permalink
Merge 60e9636 into cbbd508
Browse files Browse the repository at this point in the history
  • Loading branch information
zchsh committed Oct 4, 2022
2 parents cbbd508 + 60e9636 commit 52d500b
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 30 deletions.
5 changes: 5 additions & 0 deletions .changeset/shy-eyes-scream.md
@@ -0,0 +1,5 @@
---
'@hashicorp/react-code-block': minor
---

Support copy-button copying of shell-session snippets with multiple single-line commands. Support select-copy-paste copying of shell-session snippets by setting user-select none on shell-symbols.
85 changes: 69 additions & 16 deletions packages/code-block/docs.mdx
Expand Up @@ -5,7 +5,7 @@ componentName: 'CodeBlock'
A formatted code block for displaying lovely lines of code.

<LiveComponent>
{`<CodeBlock language="shell-session" code={\`<span class="token command"><span class="token shell-symbol important">$</span> <span class="token bash language-bash"><span class="token builtin class-name">echo</span> <span class="token string">"hello world!"</span></span></span>\`} />`}
{`<CodeBlock language="shell-session" code={\`<span class="token command"><span class="token shell-symbol important">$</span> <span class="token bash language-bash"><span class="token builtin class-name">echo</span> <span class="token string">"hello world!"</span></span></span>\`} />`}
</LiveComponent>

<UsageDetails packageJson={packageJson} />
Expand Down Expand Up @@ -63,9 +63,9 @@ function hello() {
`Result`

<CodeBlock
options={{ showClipboard: true}}
options={{ showClipboard: true }}
theme="dark"
code={`<span class="token keyword">const</span> foo <span class="token operator">=</span> <span class="token string">'bar'</span>\n<span class="token keyword">function</span> <span class="token function">hello</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>\n{/* */} <span class="token keyword control-flow">return</span> <span class="token known-class-name class-name">Math</span><span class="token punctuation">.</span><span class="token method function property-access">random</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">&gt;</span> <span class="token number">0.5</span> <span class="token operator">?</span> <span class="token string">'Hello'</span> <span class="token operator">:</span> <span class="token string">'Bonjour'</span>\n<span class="token punctuation">}</span>`}
code={`<span class="token keyword">const</span> foo <span class="token operator">=</span> <span class="token string">'bar'</span>\n<span class="token keyword">function</span> <span class="token function">hello</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>\n <span class="token keyword control-flow">return</span> <span class="token known-class-name class-name">Math</span><span class="token punctuation">.</span><span class="token method function property-access">random</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">&gt;</span> <span class="token number">0.5</span> <span class="token operator">?</span> <span class="token string">'Hello'</span> <span class="token operator">:</span> <span class="token string">'Bonjour'</span>\n<span class="token punctuation">}</span>`}
/>

#### Copy to Clipboard
Expand Down Expand Up @@ -110,10 +110,11 @@ function sayHello() {
`Result`

<CodeBlock
theme="dark"
code={`<span class="token comment">// Doing stuff you shouldn't copy</span>
language="javascript"
theme="dark"
code={`<span class="token comment">// Doing stuff you shouldn't copy</span>
<span class="token keyword">function</span> <span class="token function">sayHello</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
{/* */} <span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span><span class="token string">'Hello again'</span><span class="token punctuation">)</span>
<span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span><span class="token string">'Hello again'</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>`}
/>

Expand All @@ -137,19 +138,61 @@ hello world
`Result`

<CodeBlock
language="shell-session"
options={{ showClipboard: true }}
code={`<span class="token shell-symbol important">$</span> echo "hello world"\nhello world`}
/>

The following multi-line command will be copied in similar way. Note that when using multi-line commands, subsequent lines such as output or additional commands should be placed in a separate code block, as they will not be copied correctly if they are placed directly after a multi-line command.

`Source`

````
```shell-session
$ vault kv put kv-v1/dev/config/mongodb \
url=foo.example.com:35533 \
db_name=users \
username=admin password=passw0rd
```
````

`Result`

<CodeBlock
language="shell-session"
options={{ showClipboard: true }}
code={`<span class="token command"><span class="token shell-symbol important">$</span> <span class="token bash language-bash">vault kv put kv-v1/dev/config/mongodb <span class="token punctuation">\\</span></span></span>\n<span class="token command"><span class="token bash language-bash"> <span class="token assign-left variable">url</span><span class="token operator">=</span>foo.example.com:35533 <span class="token punctuation">\\</span></span></span>\n<span class="token command"><span class="token bash language-bash"> <span class="token assign-left variable">db_name</span><span class="token operator">=</span>users <span class="token punctuation">\\</span></span></span>\n<span class="token command"><span class="token bash language-bash"> <span class="token assign-left variable">username</span><span class="token operator">=</span>admin <span class="token assign-left variable">password</span><span class="token operator">=</span>passw0rd</span></span>`}
/>

The following multi-command snippet should yield two copied lines, both without the leading `$` shell symbol. As well, selecting the code should not copy the shell symbol, or the space after it.

`Source`

````
```shell-session
$ brew tap hashicorp/tap
$ brew tap hashicorp/tap/terraform
```
````

`Result`

<CodeBlock
language="shell-session"
options={{ showClipboard: true }}
code={`<span class="token shell-symbol important">$</span> brew tap hashicorp/tap\n<span class="token shell-symbol important">$</span> brew install hashicorp/tap/terraform`}
/>

Further examples:

| Scenario | Supported | What gets copied | Example |
| --------------------------------- | --------- | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| Single line commands || The command will be copied without the leading `$`. | [🔗](https://learn.hashicorp.com/vault/secrets-management/sm-static-secrets#step-5-retrieve-the-secrets) |
| Multi-line commands || The multi-line command will be copied, without the leading `$`. | [🔗](https://learn.hashicorp.com/vault/secrets-management/sm-static-secrets#q-how-do-i-save-multiple-values-at-once) |
| Commands with output || The command will be copied without the leading `$` or the output. | [🔗](https://learn.hashicorp.com/vault/secrets-management/sm-static-secrets#additional-discussion) |
| Non-shell Snippets || The entire snippet will be copied. | [🔗](https://learn.hashicorp.com/nomad/getting-started/jobs#modifying-a-job) |
| Multi-line with multiple commands || Not supported. Only the first command will be copied. | 🛑 |
| Scenario | Supported | What gets copied | Example |
| ------------------------------------------ | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| Single line commands || The command will be copied without the leading `$`. | [🔗](https://learn.hashicorp.com/vault/secrets-management/sm-static-secrets#step-5-retrieve-the-secrets) |
| Multi-line commands || The multi-line command will be copied, without the leading `$`. Do not include lines after the multi-line command, such as output or additional commands, as they will not be processed correctly. | [🔗](https://learn.hashicorp.com/vault/secrets-management/sm-static-secrets#q-how-do-i-save-multiple-values-at-once) |
| Commands with output || The command will be copied without the leading `$` or the output. | [🔗](https://learn.hashicorp.com/vault/secrets-management/sm-static-secrets#additional-discussion) |
| Non-shell Snippets || The entire snippet will be copied. | [🔗](https://learn.hashicorp.com/nomad/getting-started/jobs#modifying-a-job) |
| Single-line commands across multiple lines || Commands will be copied without their leading `$ `. Lines that do not start in `$ ` will be omitted from the copied snippet. | ⏳ (none yet, relatively new feature) |
| Multi-line with multiple commands || Not supported. Only the first command will be copied. | 🛑 |

#### Line Numbering

Expand Down Expand Up @@ -204,7 +247,7 @@ function logAThirdLine() {
`Result`

<CodeBlock
options={{ lineNumbers: true, highlight: "3,5", showClipboard: true }}
options={{ lineNumbers: true, highlight: '3,5', showClipboard: true }}
code={`<span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span><span class="token string">'Hello world!'</span><span class="token punctuation">)</span>
<span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span><span class="token string">'This is a second line.'</span><span class="token punctuation">)</span>
<span class="token comment">// Doing more stuff</span>
Expand Down Expand Up @@ -403,7 +446,12 @@ acl {
<CodeTabs theme="dark">

<CodeBlock
options={{ showClipboard: true, filename: "consul-acl.hcl", lineNumbers: true, highlight: "4,7-9" }}
options={{
showClipboard: true,
filename: 'consul-acl.hcl',
lineNumbers: true,
highlight: '4,7-9',
}}
language="json"
code={`<span class="token punctuation">{</span>
{/* */} <span class="token property">"primary_datacenter"</span><span class="token operator">:</span> <span class="token string">"dc1"</span><span class="token punctuation">,</span>
Expand All @@ -419,7 +467,12 @@ acl {
/>

<CodeBlock
options={{ showClipboard: true, filename: "consul-acl.json", lineNumbers: true, highlight: "3,6-8" }}
options={{
showClipboard: true,
filename: 'consul-acl.json',
lineNumbers: true,
highlight: '3,6-8',
}}
language="hcl"
code={`<span class="token property">primary_datacenter</span> <span class="token punctuation">=</span> <span class="token string">"dc1"</span>
<span class="token keyword">acl</span> <span class="token punctuation">{</span>
Expand Down
7 changes: 7 additions & 0 deletions packages/code-block/partials/code-lines/style.module.css
Expand Up @@ -12,6 +12,13 @@ pre.pre {
padding: 0;
margin: 0;
background-color: transparent;

/* Prevent shell-symbols from being selected, for better select-copy-paste */
&:global(.language-shell-session) {
& :global(.token.shell-symbol) {
user-select: none;
}
}
}

.code {
Expand Down
58 changes: 44 additions & 14 deletions packages/code-block/utils/process-snippet.js
Expand Up @@ -18,20 +18,50 @@ function processSnippet(snippet) {
* @param {string} snippet
*/
function parseShellSnippet(snippet) {
const firstLine = snippet.split('\n')[0]
// Is this shell snippet multiline?
const isMultiLine = firstLine.endsWith('\\') || firstLine.endsWith('EOF')
// If not multiline, return the single line snippet with leading '$ ' removed
if (!isMultiLine) return firstLine.replace('$ ', '')
// Else, return the multiline snippet formatted with shellwords escape & split
const multiLineFmt = shellwords.escape(snippet).replace('\\$\\', '')
// @TODO - re-assess use of shellwords.split() here. What are we trying to accomplish?
// It seems this splits commands into distinct tokens - but we want to copy all
// tokens of the command in their original format, don't we?
// ref: https://github.com/jimmycuadra/shellwords (really light on docs)
// ref: https://ruby-doc.org/stdlib-1.9.3/libdoc/shellwords/rdoc/Shellwords.html (ruby module of same name, has better docs)
// asana task: https://app.asana.com/0/1100423001970639/1199504357822173/f
return shellwords.split(multiLineFmt).join(' ')
const lines = snippet.split('\n')
/**
* Handle multi-line snippets.
*
* TODO: ideally we would detect many multi-line commands throughout
* a snippet. We could potentially build some complex string-based logic to
* do this... but it might be more efficient and effective to instead
* process the `code` passed to `HiddenCopyContent`:
* - We have incoming highlighted HTML or JSX, so we might be able to use
* the highlight token classes (eg "shell-symbol") to only copy commands.
* - However, our highlighter currently doesn't distinguish between
* "commands" and "output" tokens for the "shell-session" language...
* So maybe complex string-based logic would be the way to go if
* we want to support multiple multi-line commands in a single snippet?
* - Or maybe just need to look into `shellwords` use here?
* (Full disclosure, I didn't set it up, so am not familiar.
* it seems like it could be part of some solution?)
*
* ref: https://github.com/jimmycuadra/shellwords (really light on docs)
* ref: https://ruby-doc.org/stdlib-1.9.3/libdoc/shellwords/rdoc/Shellwords.html
* (ruby module of same name, seems to have better docs)
*
* Related task:
* https://app.asana.com/0/1100423001970639/1199504357822173/f
*
*/
const firstLine = lines[0]
const isMultiLineCommand =
firstLine.endsWith('\\') || firstLine.endsWith('EOF')
if (isMultiLineCommand) {
// If this is a multi-line snippet, return it formatted with shellwords escape & split
const multiLineFmt = shellwords.escape(snippet).replace('\\$\\', '')
return shellwords.split(multiLineFmt).join(' ')
}
/**
* Otherwise, we return only lines of the shell snippet that start with `$`.
* We remove the `$ ` at the start of each line.
* We lines that don't start with `$ ` - these are assumed to be output lines.
*/
const commandRegex = /^\$ /
return lines
.filter((line) => line.match(commandRegex))
.map((line) => line.replace(commandRegex, ''))
.join('\n')
}

export default processSnippet

0 comments on commit 52d500b

Please sign in to comment.