Skip to content

Commit

Permalink
Merge bbdedb3 into 37cc019
Browse files Browse the repository at this point in the history
  • Loading branch information
zchsh committed Aug 9, 2023
2 parents 37cc019 + bbdedb3 commit 7dacc50
Show file tree
Hide file tree
Showing 5 changed files with 312 additions and 115 deletions.
5 changes: 5 additions & 0 deletions .changeset/two-impalas-speak.md
@@ -0,0 +1,5 @@
---
'@hashicorp/react-code-block': minor
---

Implements support for code wrapping, through an options.wrapCode boolean property.
54 changes: 52 additions & 2 deletions packages/code-block/docs.mdx
Expand Up @@ -34,6 +34,9 @@ Longer lines of code may take up more space than the available content width. In
````
```
A line that goes on for a very long time so that it overflows the container in which it is located, which might be a pretty wide container.
This is a second line of code.
And a third line.
And another line, this is the fourth line.
```
````

Expand All @@ -42,7 +45,54 @@ A line that goes on for a very long time so that it overflows the container in w
<CodeBlock
options={{ showClipboard: true }}
theme="dark"
code={`A line that goes on for a very long time so that it overflows the container in which it is located, which might be a pretty wide container.`}
code={`A line that goes on for a very long time so that it overflows the container in which it is located, which might be a pretty wide container.\nThis is a second line of code.\nAnd a third line.\nAnd another line, this is the fourth line.`}
/>

Note this also works with line numbers and line highlighting.

<CodeBlock
options={{ showClipboard: true, lineNumbers: true, highlight: '1,3' }}
theme="dark"
code={`A line that goes on for a very long time so that it overflows the container in which it is located, which might be a pretty wide container.\nThis is a second line of code.\nAnd a third line.\nAnd another line, this is the fourth line.`}
/>

#### Wrap Code

In cases where wrapping code to new lines is preferred over horizontal scrolling, the `options.wrapCode` prop can be set to `true`. Note that this option is not yet supported in MDX contexts.

`Source`

````
```
A line that goes on for a very long time so that it overflows the container in which it is located, which might be a pretty wide container.
This is a second line of code.
And a third line.
And another line, this is the fourth line.
```
````

`Result`

<CodeBlock
options={{
showClipboard: true,
wrapCode: true,
}}
theme="dark"
code={`A line that goes on for a very long time so that it overflows the container in which it is located, which might be a pretty wide container.\nThis is a second line of code.\nAnd a third line.\nAnd another line, this is the fourth line.`}
/>

Note this also works with line numbers and line highlighting.

<CodeBlock
options={{
showClipboard: true,
lineNumbers: true,
wrapCode: true,
highlight: '1,3',
}}
theme="dark"
code={`A line that goes on for a very long time so that it overflows the container in which it is located, which might be a pretty wide container.\nThis is a second line of code.\nAnd a third line.\nAnd another line, this is the fourth line.`}
/>

#### Syntax Highlighting
Expand Down Expand Up @@ -219,7 +269,7 @@ function hello() {
options={{ lineNumbers: true, showClipboard: true }}
code={`<span class="token keyword">const</span> foo <span class="token operator">=</span> <span class="token string">'bar'</span>
<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>
{/* */} <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>
<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>
<span class="token punctuation">}</span>`}
/>

Expand Down
4 changes: 4 additions & 0 deletions packages/code-block/index.tsx
Expand Up @@ -27,6 +27,7 @@ export interface CodeBlockOptions {
showWindowBar?: boolean
filename?: string
heading?: string
wrapCode?: boolean
}

export interface CodeBlockProps {
Expand All @@ -52,6 +53,7 @@ function CodeBlock({
lineNumbers: false,
showClipboard: false,
showWindowBar: false,
wrapCode: false,
},
}: CodeBlockProps) {
const copyRef = useRef<HTMLPreElement>()
Expand All @@ -76,6 +78,7 @@ function CodeBlock({
lineNumbers,
showClipboard,
showWindowBar,
wrapCode,
} = options
if (showWindowBar) {
console.warn(
Expand Down Expand Up @@ -119,6 +122,7 @@ function CodeBlock({
highlight={highlight}
lineNumbers={lineNumbers}
hasFloatingCopyButton={hasFloatingCopyButton}
wrapCode={wrapCode}
/>
{hasFloatingCopyButton ? (
<div className={s.copyButtonContainer}>
Expand Down
209 changes: 149 additions & 60 deletions packages/code-block/partials/code-lines/index.js
Expand Up @@ -3,105 +3,194 @@
* SPDX-License-Identifier: MPL-2.0
*/

/**
* Note: lines of code are expected to be stable. If we need to work with
* dynamic code blocks in the future, we could assign random unique IDs
* to each line during the `linesOfCode` `useMemo` function.
*
* For now, we disable react/no-array-index key for the entire file.
*/
/* eslint-disable react/no-array-index-key */

import React, { useMemo } from 'react'
import classNames from 'classnames'
import parseHighlightedLines from '../../utils/parse-highlighted-lines'
import splitJsxIntoLines from './utils/split-jsx-into-lines'
import splitHtmlIntoLines from './utils/split-html-into-lines'
import classNames from 'classnames'
import s from './style.module.css'

/**
* Render the provided code into separate line elements,
* accounting for all provided options.
*/
function CodeLines({
code,
language,
lineNumbers,
highlight,
hasFloatingCopyButton,
wrapCode,
}) {
// Parse out an array of integers representing which lines to highlight
const highlightedLines = parseHighlightedLines(highlight)

/**
* Split the incoming code into lines.
* We need to do this in order to render each line of code in a
* separate element, which is necessary for features such as highlighting
* specific lines and allowing code to wrap.
*/
const linesOfCode = useMemo(() => {
const isHtmlString = typeof code === 'string'
return isHtmlString ? splitHtmlIntoLines(code) : splitJsxIntoLines(code)
}, [code])
const lineElements = isHtmlString
? splitHtmlIntoLines(code)
: splitJsxIntoLines(code)
const lineCount = lineElements.length
const padLevel = Math.max(lineCount.toString().length, 1)
return lineElements.map((children, index) => {
const number = index + 1
const numberPadded = number.toString().padEnd(padLevel)
const highlight = highlightedLines.indexOf(number) !== -1
const dim = highlightedLines.length > 0 && !highlight
return { children, number: numberPadded, highlight, dim }
})
}, [code, highlightedLines])

const lineCount = linesOfCode.length
const highlightedLines = parseHighlightedLines(highlight)
// When rendering wrapped code with line numbers shown, we need a spacer value
// that matches the padding inset of all other line numbers
let numberSpacer = null
if (lineNumbers) {
const padLevel = Math.max(linesOfCode.length.toString().length, 1)
numberSpacer = ''.padEnd(padLevel)
}

return (
<pre className={classNames(s.pre, `language-${language}`)}>
<code className={classNames(s.code, `language-${language}`)}>
// When the floating copy button is present, we add padding to many lines
const padRight = hasFloatingCopyButton

if (wrapCode) {
/**
* For wrapped code, we use a single-column flex layout.
* Lines of code are stacked in a single container, and each line row renders
* its own line number, which ensures that when lines wrap, the line numbers
* are aligned as expected
*/
return (
<PreCode language={language}>
<div className={s.wrappedLinesContainer}>
<WrappedLinesSpacer number={numberSpacer} padRight={padRight} />
{linesOfCode.map(({ number, children, highlight, dim }, idx) => (
<div className={s.wrappedLine} key={idx}>
{lineNumbers ? (
<LineNumber {...{ number, highlight, dim }} wrap />
) : null}
<LineOfCode {...{ highlight, dim, padRight }} wrap>
{children}
{'\n'}
</LineOfCode>
</div>
))}
<WrappedLinesSpacer number={numberSpacer} padRight={padRight} />
</div>
</PreCode>
)
} else {
/**
* For overflowing code, we use a two-column layout.
* The first column contains line numbers, and is effectively fixed.
* The second column contains the lines themselves, and is an overflow
* container to allow extra long lines to scroll as needed.
*/
return (
<PreCode language={language}>
{lineNumbers ? (
<span className={s.numbersColumn}>
{linesOfCode.map((_lineChildren, stableIdx) => {
const number = stableIdx + 1
const isHighlighted = highlightedLines.indexOf(number) !== -1
const isNotHighlighted =
highlightedLines.length > 0 && !isHighlighted
return (
<LineNumber
// This array is stable, so we can use index as key
// eslint-disable-next-line react/no-array-index-key
key={stableIdx}
number={number}
lineCount={lineCount}
isHighlighted={isHighlighted}
isNotHighlighted={isNotHighlighted}
/>
)
})}
{linesOfCode.map(({ number, highlight, dim }, idx) => (
<LineNumber key={idx} {...{ number, highlight, dim }} />
))}
</span>
) : null}
<span className={classNames(s.linesColumn, s.styledScrollbars)}>
<span className={s.linesWrapper}>
{linesOfCode.map((lineChildren, stableIdx) => {
const number = stableIdx + 1
const isHighlighted = highlightedLines.indexOf(number) !== -1
const isNotHighlighted = highlightedLines.length && !isHighlighted
return (
<LineOfCode
// This array is stable, so we can use index as key
// eslint-disable-next-line react/no-array-index-key
key={stableIdx}
isHighlighted={isHighlighted}
isNotHighlighted={isNotHighlighted}
hasFloatingCopyButton={hasFloatingCopyButton}
>
{lineChildren}
{'\n'}
</LineOfCode>
)
})}
<span className={s.linesScrollContainer}>
{linesOfCode.map(({ children, highlight, dim }, idx) => (
<LineOfCode key={idx} {...{ highlight, dim, padRight }}>
{children}
{'\n'}
</LineOfCode>
))}
</span>
</span>
</PreCode>
)
}
}

/**
* Set up the `<pre>` + `<code>` container
* which is necessary for language-specific syntax highlighting styles
*/
function PreCode({ children, language }) {
return (
<pre className={classNames(s.pre, `language-${language}`)}>
<code className={classNames(s.code, `language-${language}`)}>
{children}
</code>
</pre>
)
}

function LineNumber({ number, isHighlighted, isNotHighlighted, lineCount }) {
const padLevel = Math.max(lineCount.toString().length, 1)
const paddedNumber = number.toString().padEnd(padLevel)
/**
* Provides "padding" at the top and bottom of a code block with wrapping lines
* while retaining the "numbers" and "lines" separation border.
*
* For context, with wrapped code, we don't have separate "numbers" and "lines"
* columns as we would with overflowing code. So, we can't add padding
* to those columns as we do with overflowing code.
*
* To create padding-equivalent space, while also rendering a continuous border
* between the "numbers" and "lines" columns, we use this component,
* which is essentially and empty line of code that's been shortened a bit.
*/
function WrappedLinesSpacer({ number, padRight }) {
return (
<div className={s.wrappedLinesSpacer}>
{number ? <LineNumber number={number} wrap /> : null}
<LineOfCode padRight={padRight}>{'\n'}</LineOfCode>
</div>
)
}

/**
* Renders a line number.
*
* Note the `number` is rendered in monospace in a whitespace-sensitive way,
* so that if a padded string is passed, we can allow for table-like alignment
* of line numbers, and consistent horizontal width of numbers across all lines.
*/
function LineNumber({ number, highlight, dim, wrap }) {
return (
<span
className={classNames(s.LineNumber, {
[s.isHighlighted]: isHighlighted,
[s.isNotHighlighted]: isNotHighlighted,
[s.highlight]: highlight,
[s.dim]: dim,
[s.wrap]: wrap,
})}
dangerouslySetInnerHTML={{ __html: paddedNumber }}
/>
>
{number}
</span>
)
}

function LineOfCode({
children,
isHighlighted,
isNotHighlighted,
hasFloatingCopyButton,
}) {
/**
* Renders a line of code
*/
function LineOfCode({ children, highlight, dim, padRight, wrap }) {
return (
<span
className={classNames(s.LineOfCode, {
[s.isHighlighted]: isHighlighted,
[s.isNotHighlighted]: isNotHighlighted,
[s.hasFloatingCopyButton]: hasFloatingCopyButton,
[s.highlight]: highlight,
[s.dim]: dim,
[s.padRight]: padRight,
[s.wrap]: wrap,
})}
>
{children}
Expand Down

0 comments on commit 7dacc50

Please sign in to comment.