diff --git a/code/lib/components/src/syntaxhighlighter/syntaxhighlighter-types.ts b/code/lib/components/src/syntaxhighlighter/syntaxhighlighter-types.ts index 7548a537a72e..7b90c5ed9094 100644 --- a/code/lib/components/src/syntaxhighlighter/syntaxhighlighter-types.ts +++ b/code/lib/components/src/syntaxhighlighter/syntaxhighlighter-types.ts @@ -7,6 +7,8 @@ export interface SyntaxHighlighterRendererProps { useInlineStyles: boolean; } +export type SyntaxHighlighterRenderer = (props: SyntaxHighlighterRendererProps) => ReactNode; + export interface SyntaxHighlighterCustomProps { language: string; copyable?: boolean; @@ -15,7 +17,7 @@ export interface SyntaxHighlighterCustomProps { format?: SyntaxHighlighterFormatTypes; formatter?: (type: SyntaxHighlighterFormatTypes, source: string) => string; className?: string; - renderer?: (props: SyntaxHighlighterRendererProps) => ReactNode; + renderer?: SyntaxHighlighterRenderer; } export type SyntaxHighlighterFormatTypes = boolean | 'dedent' | BuiltInParserName; diff --git a/code/lib/components/src/syntaxhighlighter/syntaxhighlighter.tsx b/code/lib/components/src/syntaxhighlighter/syntaxhighlighter.tsx index 340c72a62b2e..ec55a4059a2a 100644 --- a/code/lib/components/src/syntaxhighlighter/syntaxhighlighter.tsx +++ b/code/lib/components/src/syntaxhighlighter/syntaxhighlighter.tsx @@ -1,11 +1,4 @@ -import React, { - ClipboardEvent, - ComponentProps, - FC, - MouseEvent, - useCallback, - useState, -} from 'react'; +import React, { ComponentProps, FC, MouseEvent, useCallback, useState } from 'react'; import { logger } from '@storybook/client-logger'; import { styled } from '@storybook/theming'; import global from 'global'; @@ -36,11 +29,17 @@ import typescript from 'react-syntax-highlighter/dist/esm/languages/prism/typesc // @ts-ignore import ReactSyntaxHighlighter from 'react-syntax-highlighter/dist/esm/prism-light'; +// @ts-ignore +import { createElement } from 'react-syntax-highlighter/dist/esm/index'; import { ActionBar } from '../ActionBar/ActionBar'; import { ScrollArea } from '../ScrollArea/ScrollArea'; -import type { SyntaxHighlighterProps } from './syntaxhighlighter-types'; +import type { + SyntaxHighlighterProps, + SyntaxHighlighterRenderer, + SyntaxHighlighterRendererProps, +} from './syntaxhighlighter-types'; const { navigator, document, window: globalWindow } = global; @@ -83,6 +82,7 @@ export function createCopyToClipboardFunction() { export interface WrapperProps { bordered?: boolean; padded?: boolean; + showLineNumbers?: boolean; } const Wrapper = styled.div( @@ -98,6 +98,15 @@ const Wrapper = styled.div( borderRadius: theme.borderRadius, background: theme.background.content, } + : {}, + ({ showLineNumbers }) => + showLineNumbers + ? { + // use the before pseudo element to display line numbers + '.react-syntax-highlighter-line-number::before': { + content: 'attr(data-line-number)', + }, + } : {} ); @@ -138,6 +147,52 @@ const Code = styled.div(({ theme }) => ({ opacity: 1, })); +const processLineNumber = (row: any) => { + const children = [...row.children]; + const lineNumberNode = children[0]; + const lineNumber = lineNumberNode.children[0].value; + const processedLineNumberNode = { + ...lineNumberNode, + // empty the line-number element + children: [], + properties: { + ...lineNumberNode.properties, + // add a data-line-number attribute to line-number element, so we can access the line number with `content: attr(data-line-number)` + 'data-line-number': lineNumber, + // remove the 'userSelect: none' style, which will produce extra empty lines when copy-pasting in firefox + style: { ...lineNumberNode.properties.style, userSelect: 'auto' }, + }, + }; + children[0] = processedLineNumberNode; + return { ...row, children }; +}; + +/** + * A custom renderer for handling `span.linenumber` element in each line of code, + * which is enabled by default if no renderer is passed in from the parent component + */ +const defaultRenderer: SyntaxHighlighterRenderer = ({ rows, stylesheet, useInlineStyles }) => { + return rows.map((node: any, i: number) => { + return createElement({ + node: processLineNumber(node), + stylesheet, + useInlineStyles, + key: `code-segement${i}`, + }); + }); +}; + +const wrapRenderer = (renderer: SyntaxHighlighterRenderer, showLineNumbers: boolean) => { + if (!showLineNumbers) { + return renderer; + } + if (renderer) { + return ({ rows, ...rest }: SyntaxHighlighterRendererProps) => + renderer({ rows: rows.map((row) => processLineNumber(row)), ...rest }); + } + return defaultRenderer; +}; + export interface SyntaxHighlighterState { copied: boolean; } @@ -163,25 +218,24 @@ export const SyntaxHighlighter: FC = ({ const highlightableCode = formatter ? formatter(format, children) : children.trim(); const [copied, setCopied] = useState(false); - const onClick = useCallback( - (e: MouseEvent | ClipboardEvent) => { - e.preventDefault(); - - const selectedText = globalWindow.getSelection().toString(); - const textToCopy = e.type !== 'click' && selectedText ? selectedText : highlightableCode; - - copyToClipboard(textToCopy) - .then(() => { - setCopied(true); - globalWindow.setTimeout(() => setCopied(false), 1500); - }) - .catch(logger.error); - }, - [] - ); + const onClick = useCallback((e: MouseEvent) => { + e.preventDefault(); + copyToClipboard(highlightableCode) + .then(() => { + setCopied(true); + globalWindow.setTimeout(() => setCopied(false), 1500); + }) + .catch(logger.error); + }, []); + const renderer = wrapRenderer(rest.renderer, showLineNumbers); return ( - + = ({ CodeTag={Code} lineNumberContainerStyle={{}} {...rest} + renderer={renderer} > {highlightableCode}