Skip to content

Commit

Permalink
refactor: make it composable
Browse files Browse the repository at this point in the history
  • Loading branch information
zchsh committed Aug 9, 2023
1 parent 6bab8c4 commit bbdedb3
Show file tree
Hide file tree
Showing 3 changed files with 264 additions and 217 deletions.
40 changes: 31 additions & 9 deletions packages/code-block/docs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,25 @@ 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.
```
````

`Result`

<CodeBlock
options={{ showClipboard: true }}
theme="light"
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.`}
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 }}
theme="light"
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.`}
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
Expand All @@ -60,17 +64,35 @@ In cases where wrapping code to new lines is preferred over horizontal scrolling

````
```
A line that goes on for a very long time so that it would overflow the container in which it is located, which might be a pretty wide container, but it wraps instead.
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, lineNumbers: true }}
theme="light"
code={`A line that goes on for a very long time so that it would overflow the container in which it is located, which might be a pretty wide container, but it wraps instead.\nThis is a second line of code.`}
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
271 changes: 145 additions & 126 deletions packages/code-block/partials/code-lines/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,26 @@
* 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,
Expand All @@ -18,160 +31,166 @@ function CodeLines({
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}`)}>
{lineNumbers && !wrapCode ? (
// 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}
wrapCode={false}
/>
)
})}
{linesOfCode.map(({ number, highlight, dim }, idx) => (
<LineNumber key={idx} {...{ number, highlight, dim }} />
))}
</span>
) : null}
<span
className={classNames(s.linesColumn, s.styledScrollbars, {
[s.wrapCode]: wrapCode,
})}
>
{wrapCode ? (
<LineSpacer
lineCount={lineCount}
hasFloatingCopyButton={hasFloatingCopyButton}
/>
) : null}
<span
className={classNames(s.linesWrapper, { [s.wrapCode]: wrapCode })}
>
{linesOfCode.map((lineChildren, stableIdx) => {
const number = stableIdx + 1
const isHighlighted = highlightedLines.indexOf(number) !== -1
const isNotHighlighted = highlightedLines.length && !isHighlighted
return (
// This array is stable, so we can use index as key
// eslint-disable-next-line react/no-array-index-key
<div className={s.lineWrapper} key={stableIdx}>
{lineNumbers && wrapCode ? (
<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}
wrapCode={true}
/>
) : null}
<LineOfCode
isHighlighted={isHighlighted}
isNotHighlighted={isNotHighlighted}
hasFloatingCopyButton={hasFloatingCopyButton}
wrapCode={wrapCode}
>
{lineChildren}
{'\n'}
</LineOfCode>
</div>
)
})}
<span className={classNames(s.linesColumn, s.styledScrollbars)}>
<span className={s.linesScrollContainer}>
{linesOfCode.map(({ children, highlight, dim }, idx) => (
<LineOfCode key={idx} {...{ highlight, dim, padRight }}>
{children}
{'\n'}
</LineOfCode>
))}
</span>
{wrapCode ? (
<LineSpacer
lineCount={lineCount}
hasFloatingCopyButton={hasFloatingCopyButton}
/>
) : null}
</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 LineSpacer({ lineCount, hasFloatingCopyButton }) {
/**
* 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={classNames(s.linesSpacer, {
[s.wrapCode]: true,
})}
>
<LineNumber
number=""
lineCount={lineCount}
isHighlighted={false}
isNotHighlighted={true}
wrapCode={true}
/>
<LineOfCode
isHighlighted={false}
isNotHighlighted={true}
hasFloatingCopyButton={hasFloatingCopyButton}
wrapCode={true}
>
{''}
{'\n'}
</LineOfCode>
<div className={s.wrappedLinesSpacer}>
{number ? <LineNumber number={number} wrap /> : null}
<LineOfCode padRight={padRight}>{'\n'}</LineOfCode>
</div>
)
}

function LineNumber({
number,
isHighlighted,
isNotHighlighted,
lineCount,
wrapCode,
}) {
const padLevel = Math.max(lineCount.toString().length, 1)
const paddedNumber = number.toString().padEnd(padLevel)
/**
* 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.wrapCode]: wrapCode,
[s.highlight]: highlight,
[s.dim]: dim,
[s.wrap]: wrap,
})}
dangerouslySetInnerHTML={{ __html: paddedNumber }}
/>
>
{number}
</span>
)
}

function LineOfCode({
children,
isHighlighted,
isNotHighlighted,
hasFloatingCopyButton,
wrapCode,
}) {
/**
* 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.wrapCode]: wrapCode,
[s.highlight]: highlight,
[s.dim]: dim,
[s.padRight]: padRight,
[s.wrap]: wrap,
})}
>
{children}
Expand Down

0 comments on commit bbdedb3

Please sign in to comment.