Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions setupTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ window.ResizeObserver = ResizeObserver;

Element.prototype.scrollIntoView = jest.fn();

jest.mock('@ably/ui/core/utils/syntax-highlighter', () => ({
jest.mock('src/utilities/syntax-highlighter', () => ({
highlightSnippet: jest.fn,
LINE_HIGHLIGHT_CLASSES: {
addition: 'code-line-addition',
Expand All @@ -23,12 +23,12 @@ jest.mock('@ably/ui/core/utils/syntax-highlighter', () => ({
splitHtmlLines: (html) => html.split('\n'),
}));

jest.mock('@ably/ui/core/Code', () => ({
jest.mock('src/components/ui/Code', () => ({
__esModule: true,
default: () => null,
}));

jest.mock('@ably/ui/core/utils/syntax-highlighter-registry', () => ({
jest.mock('src/utilities/syntax-highlighter-registry', () => ({
__esModule: true,
default: [],
}));
Expand Down
4 changes: 2 additions & 2 deletions src/components/Layout/MDXWrapper.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { WindowLocation } from '@reach/router';
import { render, screen, waitFor } from '@testing-library/react';
import { Helmet } from 'react-helmet';
import If from './mdx/If';
import CodeSnippet from '@ably/ui/core/CodeSnippet';
import CodeSnippet from 'src/components/ui/CodeSnippet';
import UserContext from 'src/contexts/user-context';
import MDXWrapper from './MDXWrapper';

Expand Down Expand Up @@ -54,7 +54,7 @@ jest.mock('src/components/Icon', () => {
});

// Mock Code component used by CodeSnippet
jest.mock('@ably/ui/core/Code', () => {
jest.mock('src/components/ui/Code', () => {
return {
__esModule: true,
default: ({ language, snippet }: any) => (
Expand Down
4 changes: 2 additions & 2 deletions src/components/Layout/MDXWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import React, {
ReactElement,
} from 'react';
import { navigate, PageProps } from 'gatsby';
import CodeSnippet from '@ably/ui/core/CodeSnippet';
import type { CodeSnippetProps, SDKType } from '@ably/ui/core/CodeSnippet';
import CodeSnippet from 'src/components/ui/CodeSnippet';
import type { CodeSnippetProps, SDKType } from 'src/components/ui/CodeSnippet';
import cn from 'src/utilities/cn';

import { getRandomChannelName } from '../../utilities/get-random-channel-name';
Expand Down
121 changes: 121 additions & 0 deletions src/components/ui/Code.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import React from 'react';
import {
highlightSnippet,
LINE_HIGHLIGHT_CLASSES,
registerDefaultLanguages,
splitHtmlLines,
} from 'src/utilities/syntax-highlighter';
import languagesRegistry from 'src/utilities/syntax-highlighter-registry';
import cn from 'src/utilities/cn';

registerDefaultLanguages(languagesRegistry);

export type LineHighlightType = 'addition' | 'removal' | 'highlight';

type CodeProps = {
language: string;
snippet: string;
textSize?: string;
padding?: string;
additionalCSS?: string;
showLines?: boolean;
lineCSS?: string;
wrap?: boolean;
lineHighlights?: Record<number, LineHighlightType>;
};

const Code = ({
language,
snippet,
textSize = 'ui-text-code',
padding = 'p-8',
additionalCSS = '',
showLines,
lineCSS,
wrap = false,
lineHighlights,
}: CodeProps) => {
// Trim the snippet and remove trailing empty lines
const trimmedSnippet = snippet.trimEnd();
const HTMLraw = highlightSnippet(language, trimmedSnippet) ?? '';
const className = `language-${language} ${textSize}`;

// Calculate line count after removing trailing empty lines
const lines = trimmedSnippet.split(/\r\n|\r|\n/);
const lineCount = lines.length;

const hasHighlights = lineHighlights && Object.keys(lineHighlights).length > 0;

// Per-line rendering when highlights are present
if (hasHighlights) {
const htmlLines = splitHtmlLines(HTMLraw);

return (
<div className={cn('hljs overflow-y-auto', padding, additionalCSS)} data-id="code">
<pre
lang={language}
className={cn(
'h-full flex-1 text-p4 leading-normal',
wrap ? 'whitespace-pre-wrap break-words' : 'overflow-x-auto',
)}
>
<code className={className}>
{htmlLines.map((lineHtml, i) => {
const lineNum = i + 1;
const highlightType = lineHighlights[lineNum];
const highlightClass = highlightType ? LINE_HIGHLIGHT_CLASSES[highlightType] : undefined;

return (
<span key={i} className={cn('flex min-w-full pl-2', highlightClass)}>
{showLines && (
<span
className={cn(
'mr-4 font-mono text-right text-neutral-800 select-none shrink-0 inline-block leading-normal',
lineCSS,
)}
style={{ minWidth: `${String(lineCount).length}ch` }}
>
{lineNum}
</span>
)}
<span
className="flex-1 !leading-normal"
dangerouslySetInnerHTML={{
__html: lineHtml || '&nbsp;',
}}
/>
</span>
);
})}
</code>
</pre>
</div>
);
}

// Default: single-block rendering (no highlights)
return (
<div className={cn('hljs overflow-y-auto flex', padding, additionalCSS)} data-id="code">
{showLines ? (
<div className="text-p4 leading-normal pt-px">
{[...Array(lineCount)].map((_, i) => (
<p className={cn('mr-4 font-mono text-right text-neutral-800', lineCSS)} key={i}>
{i + 1}
</p>
))}
</div>
) : null}
<pre
lang={language}
className={cn(
'h-full flex-1 text-p4 leading-normal',
wrap ? 'whitespace-pre-wrap break-words' : 'overflow-x-auto',
)}
>
<code className={className} dangerouslySetInnerHTML={{ __html: HTMLraw }} />
</pre>
</div>
);
};

export default Code;
Loading