Skip to content

Commit

Permalink
add focusedLines prop to CodeBlock (#112)
Browse files Browse the repository at this point in the history
  • Loading branch information
souporserious committed May 9, 2024
1 parent 080e8de commit bd646c4
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 40 deletions.
21 changes: 21 additions & 0 deletions .changeset/witty-jars-drive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
'mdxts': minor
---

Adds `focusedLines` and `unfocusedLinesOpacity` props to the `CodeBlock` component to control focusing a set of lines and dimming the other lines. It uses an image mask to dim out the lines which can be controlled using `unfocusedLinesOpacity`:

````mdx
```tsx focusedLines="3-4"
const a = 1
const b = 2
const result = a + b
console.log(result) // 3
```
````

```tsx
<CodeBlock
value={`const a = 1;\nconst b = 2;\n\nconst add = a + b\nconst subtract = a - b`}
focusedLines="2, 4"
/>
```
25 changes: 23 additions & 2 deletions packages/mdxts/src/components/CodeBlock/CodeBlock.examples.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,30 @@ export function LineNumbering() {
export function LineHighlighting() {
return (
<CodeBlock
filename="line-highlights.ts"
filename="line-highlight.ts"
value={`const a = 1;\nconst b = 2;\n\nconst add = a + b\nconst subtract = a - b`}
lineHighlights="1-2, 4"
lineHighlights="2, 4"
/>
)
}

export function LineFocusing() {
return (
<CodeBlock
filename="line-focus.ts"
value={`const a = 1;\nconst b = 2;\n\nconst add = a + b\nconst subtract = a - b`}
focusedLines="2, 4"
/>
)
}

export function LineHighlightAndFocus() {
return (
<CodeBlock
filename="line-highlight-and-focus.ts"
value={`const a = 1;\nconst b = 2;\n\nconst add = a + b\nconst subtract = a - b`}
lineHighlights="2, 4"
focusedLines="2, 4"
/>
)
}
Expand Down
44 changes: 32 additions & 12 deletions packages/mdxts/src/components/CodeBlock/CodeBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import { CopyButton } from '../CopyButton'
import { Tokens } from './Tokens'
import type { Languages } from './get-tokens'
import { getTokens } from './get-tokens'
import type { ContextValue } from './Context'
import { Context } from './Context'
import { LineHighlights } from './LineHighlights'
import { LineNumbers } from './LineNumbers'
import { Toolbar } from './Toolbar'
import { parseSourceTextMetadata } from './parse-source-text-metadata'
import { generateFocusLinesMaskImage } from './utils'

export type BaseCodeBlockProps = {
/** Name of the file. */
Expand All @@ -22,9 +24,15 @@ export type BaseCodeBlockProps = {
/** Show or hide line numbers. */
lineNumbers?: boolean

/** A string of comma separated lines and ranges to highlight. */
/** A string of comma separated lines and ranges to highlight e.g. `'1, 3-5, 7'`. */
lineHighlights?: string

/** A string of comma separated lines and ranges to focus e.g. `'6-8, 12'`. */
focusedLines?: string

/** Opacity of unfocused lines when using `focusedLines`. */
unfocusedLinesOpacity?: number

/** Whether or not to show the toolbar. */
toolbar?: boolean

Expand Down Expand Up @@ -84,6 +92,8 @@ export async function CodeBlock({
language,
lineNumbers,
lineHighlights,
focusedLines,
unfocusedLinesOpacity = 0.6,
toolbar,
allowCopy,
allowErrors,
Expand All @@ -92,7 +102,7 @@ export async function CodeBlock({
sourcePath,
...props
}: CodeBlockProps) {
const padding = props.style?.container?.padding ?? '1ch'
const padding = props.style?.container?.padding ?? '0.5lh'
const hasValue = 'value' in props
const hasSource = 'source' in props
const options: any = {}
Expand Down Expand Up @@ -123,11 +133,11 @@ export async function CodeBlock({
const contextValue = {
value: metadata.value,
filenameLabel: filename || hasSource ? metadata.filenameLabel : undefined,
highlight: lineHighlights,
lineHighlights,
padding,
sourcePath,
tokens,
}
} satisfies ContextValue

if ('children' in props) {
return <Context value={contextValue}>{props.children}</Context>
Expand All @@ -137,6 +147,9 @@ export async function CodeBlock({
const shouldRenderToolbar = Boolean(
toolbar === undefined ? filename || hasSource || allowCopy : toolbar
)
const imageMask = focusedLines
? generateFocusLinesMaskImage(focusedLines)
: undefined
const Container = shouldRenderToolbar ? 'div' : React.Fragment
const containerProps = shouldRenderToolbar
? {
Expand All @@ -151,6 +164,7 @@ export async function CodeBlock({
} satisfies React.CSSProperties,
}
: {}
const isGridLayout = Boolean(lineNumbers || lineHighlights || focusedLines)

return (
<Context value={contextValue}>
Expand All @@ -167,13 +181,11 @@ export async function CodeBlock({
shouldRenderToolbar ? undefined : props.className?.container
}
style={{
display: lineNumbers || lineHighlights ? 'grid' : undefined,
gridTemplateColumns:
lineNumbers || lineHighlights ? 'auto 1fr' : undefined,
gridTemplateRows:
lineNumbers || lineHighlights
? `repeat(${tokens.length}, 1lh)`
: undefined,
display: isGridLayout ? 'grid' : undefined,
gridTemplateColumns: isGridLayout ? 'auto 1fr' : undefined,
gridTemplateRows: isGridLayout
? `repeat(${tokens.length}, 1lh)`
: undefined,
lineHeight: 1.4,
whiteSpace: 'pre',
wordWrap: 'break-word',
Expand Down Expand Up @@ -209,11 +221,19 @@ export async function CodeBlock({
}}
/>
) : null}
{lineNumbers || lineHighlights ? (
{isGridLayout ? (
<div
style={{
gridRow: '1 / -1',
gridColumn: lineNumbers ? 2 : '1 / -1',
...(focusedLines
? {
'--m0': `rgba(0, 0, 0, ${unfocusedLinesOpacity})`,
'--m1': 'rgba(0, 0, 0, 1)',
maskImage: imageMask,
width: 'max-content',
}
: {}),
}}
>
<Tokens
Expand Down
8 changes: 5 additions & 3 deletions packages/mdxts/src/components/CodeBlock/Context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { CSSProperties } from 'react'
import { createContext } from '../../utils/context'
import type { getTokens } from './get-tokens'

export const Context = createContext<{
export type ContextValue = {
value: string
tokens: Awaited<ReturnType<typeof getTokens>>
filenameLabel?: string
sourcePath?: string | false
highlight?: string
lineHighlights?: string
padding?: CSSProperties['padding']
} | null>(null)
} | null

export const Context = createContext<ContextValue>(null)
25 changes: 3 additions & 22 deletions packages/mdxts/src/components/CodeBlock/LineHighlights.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,8 @@ import React from 'react'
import { getThemeColors } from '../../index'
import { getContext } from '../../utils/context'
import { Context } from './Context'

export type HighlightBlock = {
start: number
end: number
height: number
}

/** Parses a string of comma separated line ranges into an array of highlight blocks. */
export function getHighlights(ranges: string): HighlightBlock[] {
return ranges.split(',').map((range) => {
const [start, end] = range.split('-')
const parsedStart = parseInt(start, 10) - 1
const parsedEnd = end ? parseInt(end, 10) - 1 : parsedStart

return {
start: parsedStart,
end: parsedEnd,
height: parsedEnd - parsedStart + 1,
}
})
}
import type { HighlightBlock } from './utils'
import { getHighlights } from './utils'

/** Renders a highlight over a range of `CodeBlock` lines. */
export async function LineHighlights({
Expand All @@ -42,7 +23,7 @@ export async function LineHighlights({
}) {
const context = getContext(Context)
const theme = await getThemeColors()
const highlightRanges = highlightRangesProp || context?.highlight
const highlightRanges = highlightRangesProp || context?.lineHighlights

if (!highlightRanges) {
throw new Error(
Expand Down
2 changes: 1 addition & 1 deletion packages/mdxts/src/components/CodeBlock/LineNumbers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export async function LineNumbers({
}

const theme = await getThemeColors()
const highlightRanges = highlightRangesProp || context?.highlight
const highlightRanges = highlightRangesProp || context?.lineHighlights
const shouldHighlightLine = calculateLinesToHighlight(highlightRanges)

return (
Expand Down
52 changes: 52 additions & 0 deletions packages/mdxts/src/components/CodeBlock/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,55 @@ export function keepElementInView(

return styles
}

export type HighlightBlock = {
start: number
end: number
height: number
}

/** Parses a string of comma separated line ranges into an array of highlight blocks. */
export function getHighlights(ranges: string): HighlightBlock[] {
return ranges.split(',').map((range) => {
const [start, end] = range.split('-')
const parsedStart = parseInt(start, 10) - 1
const parsedEnd = end ? parseInt(end, 10) - 1 : parsedStart

return {
start: parsedStart,
end: parsedEnd,
height: parsedEnd - parsedStart + 1,
}
})
}

/** Generates a CSS linear gradient mask to focus highlighted lines. */
export function generateFocusLinesMaskImage(lineHighlights: string) {
const blocks = getHighlights(lineHighlights)
let maskPieces: string[] = []

if (blocks.length > 0 && blocks[0].start > 0) {
maskPieces.push(`var(--m0) ${blocks[0].start}lh`)
}

blocks.forEach((block, index) => {
const start = `${block.start}lh`
const end = `${block.end + 1}lh`

maskPieces.push(`var(--m1) ${start}, var(--m1) ${end}`)

const nextStart =
index + 1 < blocks.length ? `${blocks[index + 1].start}lh` : `100%`
if (end !== nextStart) {
maskPieces.push(`var(--m0) ${end}, var(--m0) ${nextStart}`)
}
})

// Ensure the mask ends with a solid section by adding a last stop at 100% if not already specified
const lastEnd = `${blocks[blocks.length - 1].end + 1}lh`
if (maskPieces[maskPieces.length - 1] !== `var(--m1) ${lastEnd}`) {
maskPieces.push(`var(--m0) 100%`)
}

return `linear-gradient(to bottom, ${maskPieces.join(', ')})`
}

0 comments on commit bd646c4

Please sign in to comment.