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

feat(www): add copy button to code snippets #15834

Merged
merged 28 commits into from
Jul 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d9905de
feat(www): add standalone pre component with copy functionality
DSchau Jul 17, 2019
ed4801a
chore: make it vaguely appealing
DSchau Jul 17, 2019
246eb4f
chore: make even prettier
DSchau Jul 17, 2019
d5068a8
chore: keep iterating
DSchau Jul 17, 2019
b8bb626
chore: make it work nicely again
DSchau Jul 17, 2019
b63020c
chore: get syntax highlighting working; todo line highlighting
DSchau Jul 17, 2019
bcaf18a
feat: improve accessibility (not done!)
DSchau Jul 17, 2019
16d3cbe
feat: add screenreader text
DSchau Jul 17, 2019
5755947
chore: swap to name
DSchau Jul 17, 2019
d4f06ee
chore: add missing status
DSchau Jul 17, 2019
a6fe77e
feat: get line highlighting mostly working
DSchau Jul 18, 2019
2ea5c81
feat: get hide directive working too
DSchau Jul 18, 2019
253eef5
feat: iron out highlights and use correct aria-role
DSchau Jul 18, 2019
a8eff91
chore: iron-out hide
DSchau Jul 18, 2019
083c71f
feat: add support for braces in language
DSchau Jul 18, 2019
1919a03
style: implement flo's new design
DSchau Jul 19, 2019
d37f914
test: get tests passing
DSchau Jul 19, 2019
fc095b7
Merge branch 'master' of github.com:gatsbyjs/gatsby into www/copy
DSchau Jul 19, 2019
e05e395
chore: restore monospace stack
DSchau Jul 19, 2019
7db21a8
feat: get {} working again
DSchau Jul 19, 2019
82ec189
chore: make sure to trim
DSchau Jul 19, 2019
e1feaea
chore: add some tests
DSchau Jul 19, 2019
e08c990
test: finish tests
DSchau Jul 19, 2019
d86d8a4
test: more of 'em of course
DSchau Jul 19, 2019
45d92a4
chore: more fixes
DSchau Jul 19, 2019
7a89649
test: more tests; and calling this done!
DSchau Jul 19, 2019
1742d2b
chore: tiny fix
DSchau Jul 19, 2019
1be3da0
chore: add back missing autolink headers
DSchau Jul 19, 2019
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: 0 additions & 2 deletions www/gatsby-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@ module.exports = {
gatsbyRemarkPlugins: [
`gatsby-remark-graphviz`,
`gatsby-remark-embed-video`,
`gatsby-remark-code-titles`,
{
resolve: `gatsby-remark-images`,
options: {
Expand All @@ -137,7 +136,6 @@ module.exports = {
},
},
`gatsby-remark-autolink-headers`,
`gatsby-remark-prismjs`,
`gatsby-remark-copy-linked-files`,
`gatsby-remark-smartypants`,
],
Expand Down
1 change: 1 addition & 0 deletions www/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
"mitt": "^1.1.3",
"mousetrap": "^1.6.1",
"parse-github-url": "^1.0.2",
"prism-react-renderer": "^0.1.7",
"prismjs": "^1.14.0",
"qs": "^6.5.2",
"query-string": "^6.1.0",
Expand Down
69 changes: 69 additions & 0 deletions www/src/components/code-block/__tests__/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React from "react"
import CodeBlock from ".."
import { fireEvent, render } from "react-testing-library"

beforeEach(() => {
document.execCommand = jest.fn()
})

describe(`basic functionality`, () => {
describe(`copy`, () => {
it(`renders a copy button`, () => {
const { queryByText } = render(
<CodeBlock language="jsx">{`var a = 'b'`}</CodeBlock>
)

expect(queryByText(`copy`)).toBeDefined()
})

it(`copies text to clipboard`, () => {
const text = `alert('hello world')`
const { queryByText } = render(
<CodeBlock language="jsx">{text}</CodeBlock>
)

const copyButton = queryByText(`Copy`)

fireEvent.click(copyButton)

expect(document.execCommand).toHaveBeenCalledWith(`copy`)
})
})

describe(`highlighting`, () => {
let instance
const hidden = `var a = 'i will be hidden'`
const highlighted = `
<div>
<h1>Oh shit waddup</h1>
</div>
`.trim()
beforeEach(() => {
const text = `
import React from 'react'

${hidden} // hide-line

export default function HelloWorld() {
return (
{/* highlight-start */}
${highlighted}
{/* highlight-end */}
)
}
`.trim()
instance = render(<CodeBlock language="jsx">{text}</CodeBlock>)
})

it(`hides lines appropriately`, () => {
expect(instance.queryByText(hidden)).toBeNull()
})

it(`highlights lines appropriately`, () => {
const lines = highlighted.split(`\n`)
expect(
instance.container.querySelectorAll(`.gatsby-highlight-code-line`)
).toHaveLength(lines.length)
})
})
})
203 changes: 203 additions & 0 deletions www/src/components/code-block/__tests__/normalize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import normalize from "../normalize"

describe(`highlighting`, () => {
it(`highlight-start`, () => {
expect(
normalize(
`
// highlight-start
var a = 'b'
var b = 'c'
// highlight-end
`.trim(),
`jsx`
)
).toEqual([expect.any(String), { 0: true, 1: true }])
})

it(`highlight-start, without end`, () => {
expect(
normalize(
`
var a = 'b'
// highlight-start
var b = 'c'
var d = 'e'
`.trim(),
`jsx`
)
).toEqual([expect.any(String), { 1: true, 2: true }])
})

it(`highlight-line`, () => {
expect(
normalize(
`
var a = 'b' // highlight-line
`.trim(),
`jsx`
)
).toEqual([expect.any(String), { 0: true }])
})

it(`highlight-next-line`, () => {
expect(
normalize(
`
var a = 'b' // highlight-next-line
var b = 'c'
`.trim(),
`jsx`
)
).toEqual([expect.any(String), { 1: true }])
})

it(`curly brace format`, () => {
expect(
normalize(
`
\`\`\`
var a = 'i am highlighted'
var b = 'i am not'
\`\`\`
`.trim(),
`jsx{1}`
)
).toEqual([expect.any(String), { 0: true }])
})
})

describe(`languages`, () => {
it(`handles js`, () => {
expect(
normalize(
`
function () {
alert('hi') /* highlight-linen */
}
`.trim(),
`html`
)
).toEqual([expect.any(String), { 1: true }])
})

it(`handles html`, () => {
expect(
normalize(
`
<div>
<h1>Oh shit waddup</h1> <!-- highlight-line -->
</div>
`.trim(),
`html`
)
).toEqual([expect.any(String), { 1: true }])
})

it(`handles yaml`, () => {
expect(
normalize(
`
something: true
highlighted: you bedda believe it # highlight-line
`.trim(),
`html`
)
).toEqual([expect.any(String), { 1: true }])
})

it(`handles css`, () => {
expect(
normalize(
`
p {
color: red; // highlight-line
}
`.trim(),
`css`
)
).toEqual([expect.any(String), { 1: true }])
})

it(`handles graphql`, () => {
expect(
normalize(
`
query whatever {
field # highlight-line
}
`.trim(),
`graphql`
)
).toEqual([expect.any(String), { 1: true }])
})
})

describe(`hiding`, () => {
it(`hide-line`, () => {
expect(
normalize(
`
var a = 'b' // hide-line
var b = 'c'
`.trim(),
`jsx`
)
).toEqual([`var b = 'c'`, expect.any(Object)])
})

it(`hide-start`, () => {
expect(
normalize(
`
// hide-start
var a = 'b'
var b = 'c'
// hide-end
`.trim(),
`jsx`
)
).toEqual([``, expect.any(Object)])
})

it(`hide-start without end`, () => {
expect(
normalize(
`
var a = 'b'
// hide-start
var b = 'c'
var d = 'e'
`.trim(),
`jsx`
)
).toEqual([`var a = 'b'`, expect.any(Object)])
})

describe(`next-line`, () => {
it(`on same line`, () => {
expect(
normalize(
`
var a = 'b' // hide-next-line
var b = 'c'
`.trim(),
`jsx`
)
).toEqual([`var a = 'b'`, expect.any(Object)])
})

it(`on next line`, () => {
expect(
normalize(
`
var a = 'b'
// hide-next-line
var b = 'c'
`.trim(),
`jsx`
)
).toEqual([`var a = 'b'`, expect.any(Object)])
})
})
})
96 changes: 96 additions & 0 deletions www/src/components/code-block/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React from "react"
import Highlight, { defaultProps } from "prism-react-renderer"

import Copy from "../copy"
import normalize from "./normalize"
import { fontSizes, radii, space } from "../../utils/presets"

const getParams = (name = ``) => {
const [lang, params = ``] = name.split(`:`)
return [
lang
.split(`language-`)
.pop()
.split(`{`)
.shift(),
].concat(
params.split(`&`).reduce((merged, param) => {
const [key, value] = param.split(`=`)
merged[key] = value
return merged
}, {})
)
}

/*
* MDX passes the code block as JSX
* we un-wind it a bit to get the string content
* but keep it extensible so it can be used with just children (string) and className
*/
export default ({
children,
className = children.props ? children.props.className : ``,
}) => {
const [language, { title = `` }] = getParams(className)
Copy link
Contributor

@CanRau CanRau Jan 7, 2020

Choose a reason for hiding this comment

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

@DSchau I love it 😍 and plan to use some of that to add that nice copy feature to coding4.gaiama.org too 👍 🚀

By the way mdx passes additional space separated props as well so you could get the title from props.title if you pass the title like so

```js title=gatsby-config.js

instead of

```js:title=gatsby-config.js

then it won't end up in className and you wouldn't have to extract

Yet it's working as is and would be quite a pain to migrate all docs I guess 😅

Update: just realized this PR is quite old and my comment might've not worked back in the days ^^

Copy link
Contributor

Choose a reason for hiding this comment

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

FYI: i found your described behaviour here in the mdx docs:

https://mdxjs.com/guides/live-code#code-block-meta-string

Copy link
Contributor

Choose a reason for hiding this comment

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

Uh yeah thanks for the link that's where I know this from 😁🙏

const [content, highlights] = normalize(
children.props && children.props.children
? children.props.children
: children,
className
)

return (
<Highlight
{...defaultProps}
code={content}
language={language}
theme={undefined}
>
{({ tokens, getLineProps, getTokenProps }) => (
<div className="gatsby-highlight">
{title && (
<div className="gatsby-highlight-header">
<div
className="gatsby-code-title"
css={{ fontSize: fontSizes[0] }}
>
{title}
</div>
</div>
)}
<pre className={`language-${language}`}>
<Copy
fileName={title}
css={{
position: `absolute`,
right: space[1],
top: space[1],
borderRadius: `${radii[2]}px ${radii[2]}px`,
}}
content={content}
/>
{tokens.map((line, i) => {
const lineProps = getLineProps({ line, key: i })
const className = [lineProps.className]
.concat(highlights[i] && `gatsby-highlight-code-line`)
.filter(Boolean)
.join(` `)
return (
<div
key={i}
{...Object.assign({}, lineProps, {
className,
})}
>
{line.map((token, key) => (
<span key={key} {...getTokenProps({ token, key })} />
))}
</div>
)
})}
</pre>
</div>
)}
</Highlight>
)
}
Loading