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(remark-embed-snippet): embed specific lines #21907

Merged
merged 5 commits into from
Apr 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
46 changes: 43 additions & 3 deletions packages/gatsby-remark-embed-snippet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,8 @@ The resulting HTML generated from the markdown file above would look something l

### Highlighting Lines

You can also specify specific lines for Prism to highlight using
`highlight-line` and `highlight-next-line` comments. You can also specify a
range of lines to highlight, relative to a `highlight-range` comment.
You can specify specific lines for Prism to highlight using
`highlight-line` and `highlight-next-line` comments. You can also specify a range of lines to highlight, relative to a `highlight-range` comment.

**JavaScript example**:

Expand Down Expand Up @@ -250,8 +249,49 @@ quz: "highlighted"

It's also possible to specify a range of lines to be hidden.

You can either specify line ranges in the embed using the syntax:

- #Lx - Embed one line from a file
- #Lx-y - Embed a range of lines from a file
- #Lx-y,a-b - Embed non-consecutive ranges of lines from a file

**Markdown example**:

```markdown
This is the JSX of my app:

`embed:App.js#L6-8`
```

With this example snippet:

```js
import React from "react"
import ReactDOM from "react-dom"

function App() {
return (
<div className="App">
<h1>Hello world</h1>
</div>
)
}
```

Will produce something like this:

```markdown
This is the JSX of my app:

<div className="App">
<h1>Hello world</h1>
</div>
```

**JavaScript example**:

You can also add `// hide-range` comments to your files.

```jsx
// hide-range{1-2}
import React from "react"
Expand Down
50 changes: 50 additions & 0 deletions packages/gatsby-remark-embed-snippet/src/__tests__/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,56 @@ describe(`gatsby-remark-embed-snippet`, () => {
)
})

it(`should display a code block of a single line`, () => {
const codeBlockValue = ` console.log('hello world')`
fs.readFileSync.mockReturnValue(`function test() {
${codeBlockValue}
}`)

const markdownAST = remark.parse(`\`embed:hello-world.js#L2\``)
const transformed = plugin({ markdownAST }, { directory: `examples` })

const codeBlock = transformed.children[0].children[0]

expect(codeBlock.value).toEqual(codeBlockValue)
})

it(`should display a code block of a range of lines`, () => {
const codeBlockValue = ` if (window.location.search.indexOf('query') > -1) {
console.log('The user is searching')
}`
fs.readFileSync.mockReturnValue(`function test() {
${codeBlockValue}
}`)

const markdownAST = remark.parse(`\`embed:hello-world.js#L2-4\``)
const transformed = plugin({ markdownAST }, { directory: `examples` })

const codeBlock = transformed.children[0].children[0]

expect(codeBlock.value).toEqual(codeBlockValue)
})

it(`should display a code block of a range of non-consecutive lines`, () => {
const notInSnippet = `lineShouldNotBeInSnippet();`
fs.readFileSync.mockReturnValue(`function test() {
if (window.location.search.indexOf('query') > -1) {
console.log('The user is searching')
}
}
${notInSnippet}
window.addEventListener('resize', () => {
test();
})`)

const markdownAST = remark.parse(`\`embed:hello-world.js#L2-4,7-9\``)
const transformed = plugin({ markdownAST }, { directory: `examples` })

const codeBlock = transformed.children[0].children[0]

expect(codeBlock.value).not.toContain(notInSnippet)
})

it(`should error if an invalid file path is specified`, () => {
fs.existsSync.mockImplementation(path => path !== `examples/hello-world.js`)

Expand Down
27 changes: 24 additions & 3 deletions packages/gatsby-remark-embed-snippet/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const path = require(`path`)
const fs = require(`fs`)
const normalizePath = require(`normalize-path`)
const visit = require(`unist-util-visit`)
const rangeParser = require(`parse-numeric-range`)

// Language defaults to extension.toLowerCase();
// This map tracks languages that don't match their extension.
Expand Down Expand Up @@ -46,21 +47,41 @@ module.exports = ({ markdownAST, markdownNode }, { directory } = {}) => {

if (value.startsWith(`embed:`)) {
const file = value.substr(6)
const snippetPath = normalizePath(path.join(directory, file))
let snippetPath = normalizePath(path.join(directory, file))

// Embed specific lines numbers of a file
let lines = []
const rangePrefixIndex = snippetPath.indexOf(`#L`)
if (rangePrefixIndex > -1) {
const range = snippetPath.slice(rangePrefixIndex + 2)
if (range.length === 1) {
lines = [Number.parseInt(range, 10)]
} else {
lines = rangeParser.parse(range)
}
// Remove everything after the range prefix from file path
snippetPath = snippetPath.slice(0, rangePrefixIndex)
}

if (!fs.existsSync(snippetPath)) {
throw Error(`Invalid snippet specified; no such file "${snippetPath}"`)
}

const code = fs.readFileSync(snippetPath, `utf8`).trim()
let code = fs.readFileSync(snippetPath, `utf8`).trim()
if (lines.length) {
code = code
.split(`\n`)
.filter((_, lineNumber) => lines.includes(lineNumber + 1))
.join(`\n`)
}

// PrismJS's theme styles are targeting pre[class*="language-"]
// to apply its styles. We do the same here so that users
// can apply a PrismJS theme and get the expected, ready-to-use
// outcome without any additional CSS.
//
// @see https://github.com/PrismJS/prism/blob/1d5047df37aacc900f8270b1c6215028f6988eb1/themes/prism.css#L49-L54
const language = getLanguage(file)
const language = getLanguage(snippetPath)

// Change the node type to code, insert our file as value and set language.
node.type = `code`
Expand Down