CodeHighlight
CodeHighlight
shiki 또는 highlight.js로 코드를 하이라이트해요. @mantine/code-highlight 패키지로 제공되며 MIT 라이선스로 배포돼요.
출처: 문서
본문
설치
yarn add @mantine/code-highlight
설치 후 애플리케이션의 루트에서 패키지 스타일을 import 해요:
import '@mantine/core/styles.css';
// ‼️ code-highlight 스타일은 core 패키지 스타일 다음에 import 하세요
import '@mantine/code-highlight/styles.css';
예시
CodeHighlight 컴포넌트는 문법 하이라이트와 함께 코드 스니펫을 표시하는 데 사용돼요. 원하는 어떤 코드 하이라이트 라이브러리든 사용할 수 있게 해주는 유연한 어댑터 시스템을 제공해요.
shiki로 코드 하이라이트하는 예시:
import { CodeHighlight } from '@mantine/code-highlight';
const exampleCode = `
type FilterPropsRes<T> = {
[Key in keyof T]-?: T[Key] extends undefined ? never : T[Key];
};
export function filterProps<T>(props: T) {
return Object.keys(props).reduce<FilterPropsRes<T>>((acc, key: keyof T) => {
if (props[key] !== undefined) {
acc[key] = props[key];
}
return acc;
}, {} as FilterPropsRes<T>);
}
`;
function Demo() {
return <CodeHighlight code={exampleCode} language="tsx" />;
}
어댑터
@mantine/code-highlight 패키지는 특정 코드 하이라이트 라이브러리에 의존하지 않아요. 패키지가 제공하는 기본 어댑터 중 하나를 선택하거나 직접 만들 수 있어요.
기본 어댑터:
-
createShikiAdapter– shiki 어댑터를 만들어요 -
createHighlightJsAdapter– highlight.js 어댑터를 만들어요 -
plainTextAdapter– 코드를 하이라이트하지 않고 평문으로만 표시해요(어댑터가 제공되지 않으면 기본 사용)
shiki와 함께 사용
Shiki 라이브러리는 TypeScript와 CSS/Sass 코드에 대해 가장 진보된 문법 하이라이트를 제공해요. 텍스트메이트(textmate) 문법을 사용해 코드를 하이라이트해요(VSCode와 동일). 고급 TypeScript(제네릭, props 중첩 jsx)나 CSS 코드(커스텀 문법, 최신 기능)를 하이라이트해야 한다면 Shiki 어댑터가 권장돼요. Shiki 어댑터는 Mantine 문서의 모든 코드 하이라이트에 사용돼요.
shiki 어댑터를 사용하려면 shiki 패키지를 설치해야 해요:
yarn add shiki
그런 다음 앱을 CodeHighlightAdapterProvider로 감싸고 adapter prop으로 createShikiAdapter를 제공해요:
import { MantineProvider } from '@mantine/core';
import { CodeHighlightAdapterProvider, createShikiAdapter } from '@mantine/code-highlight';
// Shiki requires async code to load the highlighter
async function loadShiki() {
const { createHighlighter } = await import('shiki');
const shiki = await createHighlighter({
langs: ['tsx', 'scss', 'html', 'bash', 'json'],
// You can load supported themes here
themes: [],
});
return shiki;
}
const shikiAdapter = createShikiAdapter(loadShiki);
function App() {
return (
<CodeHighlightAdapterProvider adapter={shikiAdapter}>
<MantineProvider>
{/* Your app here */}
</MantineProvider>
</CodeHighlightAdapterProvider>
);
}
그 후 애플리케이션에서 CodeHighlight 컴포넌트를 사용할 수 있어요:
import { CodeHighlight } from '@mantine/code-highlight';
const exampleCode = `
type FilterPropsRes<T> = {
[Key in keyof T]-?: T[Key] extends undefined ? never : T[Key];
};
export function filterProps<T>(props: T) {
return Object.keys(props).reduce<FilterPropsRes<T>>((acc, key: keyof T) => {
if (props[key] !== undefined) {
acc[key] = props[key];
}
return acc;
}, {} as FilterPropsRes<T>);
}
`;
function Demo() {
return <CodeHighlight code={exampleCode} language="tsx" />;
}
이 페이지에 나오는 이후 모든 코드 하이라이트 예시는 shiki 어댑터를 사용해요.
지연 언어 로딩
Shiki는 하이라이터에 주어진 모든 언어의 문법을 미리 로드해요. 애플리케이션이 수십 개의 언어로 코드를 하이라이트한다면, 첫 번째 코드 블록이 하이라이트되기 전에 이 모든 문법이 다운로드돼요.
문법을 필요할 때 로드하려면 createShikiAdapter에 resolveLanguage 옵션을 전달해요. 이것은 아직 하이라이터에 로드되지 않은 코드 블록의 언어로 호출되며, 반환 값은 shiki highlighter.loadLanguage에 전달돼요. 문법이 로드되면 코드가 하이라이트되고, 그 전까지는 평문으로 표시돼요:
import { createShikiAdapter } from '@mantine/code-highlight';
async function loadShiki() {
const { createHighlighter } = await import('shiki');
return createHighlighter({ langs: ['tsx'], themes: [] });
}
// Bundled highlighter resolves grammars by language name on its own
const shikiAdapter = createShikiAdapter(loadShiki, {
resolveLanguage: (language) => language,
});
shiki/core로 만든 하이라이터는 문법을 번들하지 않아요 – 주어진 언어의 문법을 import 하는 함수를 대신 반환해요:
import { createShikiAdapter } from '@mantine/code-highlight';
const grammars: Record<string, () => Promise<any>> = {
python: () => import('@shikijs/langs/python'),
ruby: () => import('@shikijs/langs/ruby'),
};
async function loadShiki() {
const { createHighlighterCore } = await import('shiki/core');
const { createJavaScriptRegexEngine } = await import('shiki/engine/javascript');
return createHighlighterCore({
langs: [],
themes: [],
engine: createJavaScriptRegexEngine(),
});
}
const shikiAdapter = createShikiAdapter(loadShiki, {
resolveLanguage: (language) => grammars[language],
});
resolveLanguage가 null 또는 undefined를 반환하면 해당 언어는 지원되지 않는 것으로 간주되고 그 코드는 평문으로 표시돼요.
사용할 수 없는 언어
코드 블록이 하이라이터에 로드되지 않았고 요청 시 로드할 수도 없는 언어를 사용하면, 오류를 던지는 대신 코드가 평문으로 표시돼요. 개발 중에는 언어마다 한 번씩 콘솔에 경고가 기록돼요 – 하이라이터의 langs 옵션에 언어를 추가하거나 resolveLanguage로 요청 시 로드해요.
문법 로드에 실패하면(예: 동적 import 실패) 코드는 평문으로 유지되고 실패는 캐시되지 않아요 – 해당 언어를 가진 코드 블록이 다음에 마운트될 때 언어가 다시 요청돼요.
highlight.js와 함께 사용
Highlight.js는 shiki보다 덜 정확한 하이라이트를 제공하지만 번들 크기가 더 작고 성능이 더 좋아요. 기본 JavaScript, HTML, CSS 코드를 하이라이트해야 한다면 highlight.js 어댑터를 선택해요.
highlight.js 어댑터를 사용하려면 highlight.js 패키지를 설치해야 해요:
yarn add highlight.js
그런 다음 앱을 CodeHighlightAdapterProvider로 감싸고 adapter prop으로 createHighlightJsAdapter를 제공해요:
import { MantineProvider } from '@mantine/core';
import { CodeHighlightAdapterProvider, createHighlightJsAdapter } from '@mantine/code-highlight';
import hljs from 'highlight.js/lib/core';
import tsLang from 'highlight.js/lib/languages/typescript';
hljs.registerLanguage('typescript', tsLang);
const highlightJsAdapter = createHighlightJsAdapter(hljs);
function App() {
return (
<CodeHighlightAdapterProvider adapter={highlightJsAdapter}>
<MantineProvider>
{/* Your app here */}
</MantineProvider>
</CodeHighlightAdapterProvider>
);
}
그런 다음 애플리케이션에 highlight.js 테마 중 하나의 스타일을 추가해야 해요. highlight.js 패키지에서 css 파일을 import 하거나 애플리케이션 head에 CDN 링크로 추가할 수 있어요:
import 'highlight.js/styles/atom-one-dark.css';
그 후 애플리케이션에서 CodeHighlight 컴포넌트를 사용할 수 있어요.
커스텀 어댑터 만들기
코드 하이라이트의 기본 동작을 향상시키거나 다른 라이브러리를 사용하고 싶다면 커스텀 어댑터를 만들 수 있어요.
커스텀 테마와 로직을 가진 커스텀 shiki 어댑터를 만드는 예시:
import { type CodeHighlightAdapter, stripShikiCodeBlocks } from '@mantine/code-highlight';
// Shiki transformers can be used to highlight diffs and other notations
// https://shiki.style/packages/transformers
import { transformerNotationDiff, transformerNotationHighlight } from '@shikijs/transformers'
// Shiki themes as objects, you can use any VSCode themes
import { darkTheme, lightTheme } from './shiki-themes';
async function loadShiki() {
const { createHighlighter } = await import('shiki');
const shiki = await createHighlighter({
langs: ['tsx', 'scss', 'html', 'bash', 'json'],
themes: [],
});
return shiki;
}
// Pass this adapter to CodeHighlightAdapterProvider component
export const customShikiAdapter: CodeHighlightAdapter = {
// loadContext is called on the client side to load the shiki highlighter
// It is required to be used if your library requires async initialization
// The value returned from loadContext is passed to getHighlighter as ctx argument
loadContext: loadShiki,
// ctx is the value returned from loadContext
// or null if loadContext is not used or has not resolved yet
getHighlighter: (ctx) => {
if (!ctx) {
return ({ code }) => ({ highlightedCode: code, isHighlighted: false });
}
return ({ code, language, colorScheme }) => ({
isHighlighted: true,
// stripShikiCodeBlocks removes
and ` tags from highlighted code
highlightedCode: stripShikiCodeBlocks(
ctx.codeToHtml(code, {
lang: language,
theme: (colorScheme === 'light' ? lightTheme : darkTheme) as any,
transformers: [transformerNotationDiff(), transformerNotationHighlight()],
})
),
});
},
};
복사 버튼
copyLabel과 copiedLabel props로 복사 버튼 라벨을 커스터마이즈할 수 있어요. 복사 버튼을 제거해야 한다면 withCopyButton={false}를 설정해요.
// Custom copy label
function Button() {
return Click me;
}
// Without copy button
function Button() {
return Click me;
}
import { CodeHighlight } from '@mantine/code-highlight';
const exampleCode = `
function Button() {
return Click me;
}
`;
function Demo() {
return (
<>
<CodeHighlight code={exampleCode} language="tsx" copyLabel="Copy code" copiedLabel="Copied!" />
<CodeHighlight code={exampleCode} language="tsx" withCopyButton={false} />
</>
);
}
탭과 함께 사용
CodeHighlightTabs 컴포넌트는 여러 코드 블록을 탭으로 구성할 수 있게 해줘요:
import { CodeHighlightTabs } from '@mantine/code-highlight';
import { tsxCode, cssCode } from './code';
function Demo() {
return (
<CodeHighlightTabs
code={[
{ fileName: 'Demo.tsx', code: tsxCode, language: 'tsx' },
{ fileName: 'Demo.module.css', code: cssCode, language: 'scss' },
]}
/>
);
}
아이콘이 있는 탭
탭 아이콘으로 어떤 React 노드든 사용할 수 있어요. 아래 예시는 @mantinex/dev-icons 패키지의 TypeScript와 CSS 아이콘을 사용하지만, 다른 아이콘 라이브러리나 커스텀 아이콘을 사용해도 돼요:
import { CodeHighlightTabs } from '@mantine/code-highlight';
import { TypeScriptIcon, CssIcon } from '@mantinex/dev-icons';
const tsxCode = `
function Button() {
return Click me;
}
`;
const cssCode = `
.button {
background-color: transparent;
color: var(--mantine-color-blue-9);
}
`;
function Demo() {
const tsIcon = <TypeScriptIcon size={16} />;
const cssIcon = <CssIcon size={16} />;
return (
<CodeHighlightTabs
code={[
{ fileName: 'Demo.tsx', code: tsxCode, language: 'tsx', icon: tsIcon },
{ fileName: 'Demo.module.css', code: cssCode, language: 'scss', icon: cssIcon },
]}
/>
);
}
파일 이름에 기반한 탭 아이콘
각 탭에 아이콘을 수동으로 제공하는 대신 getFileIcon prop을 사용해 파일 이름에 기반한 아이콘을 할당할 수 있어요. getFileIcon은 파일 이름을 받아 React 노드 또는 null을 반환해야 해요.
import { CodeHighlightTabs } from '@mantine/code-highlight';
import { TypeScriptIcon, CssIcon } from '@mantinex/dev-icons';
const tsxCode = `
function Button() {
return Click me;
}
`;
const cssCode = `
.button {
background-color: transparent;
color: var(--mantine-color-blue-9);
}
`;
function getFileIcon(fileName: string) {
if (fileName.endsWith('.ts') || fileName.endsWith('.tsx')) {
return <TypeScriptIcon size={16} />;
}
if (fileName.endsWith('.css')) {
return <CssIcon size={16} />;
}
return null;
}
function Demo() {
return (
<CodeHighlightTabs
getFileIcon={getFileIcon}
code={[
{ fileName: 'Demo.tsx', code: tsxCode, language: 'tsx' },
{ fileName: 'Demo.module.css', code: cssCode, language: 'scss' },
]}
/>
);
}
줄 번호
withLineNumbers prop을 설정해 코드 옆에 줄 번호를 표시해요:
import { CodeHighlight } from '@mantine/code-highlight';
const exampleCode = `...`;
function Demo() {
return <CodeHighlight code={exampleCode} language="tsx" withLineNumbers />;
}
첫 줄 들여쓰기
CodeHighlight는 렌더링 전에 코드를 다듬으므로 첫 줄의 들여쓰기는 제거되고 나머지 모든 줄은 들여쓰기를 유지해요. withFirstLineIndentation prop을 설정해 그것을 유지해요 – 열 정렬 텍스트나 감싸진 셸 명령처럼 첫 줄이 의도적으로 들여쓰기된 블록에 유용해요:
import { CodeHighlight } from '@mantine/code-highlight';
import { Stack, Text } from '@mantine/core';
const exampleCode = ` docker exec mongo mongodump \\
--authenticationDatabase admin \\
--out /tmp/mongo-backup
`;
function Demo() {
return (
<Stack>
<Text>Default: the first line is dedented</Text>
<CodeHighlight code={exampleCode} language="bash" />
<Text>withFirstLineIndentation</Text>
<CodeHighlight code={exampleCode} language="bash" withFirstLineIndentation />
</Stack>
);
}
시작 부분의 빈 줄과 끝의 공백은 두 경우 모두 제거돼요. 이 prop은 복사 버튼이 복사하는 코드도 제어해요.
확장 가능한 코드
코드 스니펫이 너무 길다면 withExpandButton과 defaultExpanded={false} props로 확장 가능하게 만들 수 있어요. 확장/접기 컨트롤 툴팁의 라벨을 바꾸려면 expandCodeLabel과 collapseCodeLabel을 사용해요.
import { CodeHighlight } from '@mantine/code-highlight';
function Demo() {
return (
<CodeHighlight
code="..."
language="tsx"
withExpandButton
defaultExpanded={false}
expandCodeLabel="Show full code"
collapseCodeLabel="Show less"
/>
);
}
커스텀 컨트롤
controls prop과 CodeHighlightControl 컴포넌트를 사용해 코드 블록에 커스텀 컨트롤을 추가해요:
import { CodesandboxLogoIcon, ChatCircleIcon } from '@phosphor-icons/react';
import { CodeHighlight, CodeHighlightControl } from '@mantine/code-highlight';
const exampleCode = `
function greet() {
return 'Hello, World!';
}
`;
function Demo() {
return (
<CodeHighlight
code={exampleCode}
language="ts"
controls={[
{
icon: <CodesandboxLogoIcon size={14} />,
label: 'Open in codesandbox',
onClick: () => window.open('https://codesandbox.io', '_blank'),
},
{
icon: <ChatCircleIcon size={14} />,
label: 'Comment',
onClick: () => {},
},
]}
/>
);
}
인라인 코드
InlineCodeHighlight 컴포넌트를 사용해 인라인 코드 스니펫을 하이라이트할 수 있어요:
import { Text } from '@mantine/core';
import { InlineCodeHighlight } from '@mantine/code-highlight';
function Demo() {
return (
<Text>
You can highlight code inline:{' '}
<InlineCodeHighlight
code="import { Button } from '@mantine/core'"
language="tsx"
withBorder
/>
. Is that not cool?
</Text>
);
}