# Code snippet

Shows code or a command, such as an install step or an example, on one or more lines. Long lines scroll horizontally. For code within running text, use [Inline code](/components/inline-code.md).

```tsx
// demo.tsx
import {CodeSnippet} from '@earendil-works/elanor-react';

export default function CodeSnippetDemo() {
  return <CodeSnippet>pnpm add @earendil-works/elanor-react</CodeSnippet>;
}
```

## Usage

```tsx
import {CodeSnippet} from '@earendil-works/elanor-react';

<CodeSnippet>pnpm install</CodeSnippet>;
```

Code uses `semantic.typography.code.primary` in `semantic.color.text.code` on the `semantic.color.background.fill.secondary` fill, aligned to the start.

## Copy action

Pass a [Copy button](/components/copy-button.md) to `actions`. With a mouse, the action shows on hover, on keyboard focus, and while it reports a copy; on touch screens it always shows and grows to a 48px target.

```tsx
// copy.tsx
import {CodeSnippet, CopyButton} from '@earendil-works/elanor-react';

const command = 'pnpm add @earendil-works/elanor-react';

export default function CodeSnippetCopy() {
  return (
    <CodeSnippet actions={<CopyButton value={command} aria-label="Copy command" />}>
      {command}
    </CodeSnippet>
  );
}
```

## Multiple lines

Code keeps its line breaks. Actions stay on the first line.

```tsx
// multi-line.tsx
import {CodeSnippet, CopyButton} from '@earendil-works/elanor-react';

const code = `import {Button} from '@earendil-works/elanor-react';

export function Save() {
  return <Button>Save changes</Button>;
}`;

export default function CodeSnippetMultiLine() {
  return (
    <CodeSnippet actions={<CopyButton value={code} aria-label="Copy code" />}>{code}</CodeSnippet>
  );
}
```

## Syntax highlighting

Pass highlighted lines to `HighlightedCode` as the snippet's content. Elanor includes no highlighter: use [Shiki](https://shiki.style) or [Highlights](https://diffs.com/docs#highlights) with a theme from `@earendil-works/elanor-code-themes`. The themes use the code syntax tokens, so colors follow the brand and the color scheme. Leave commands, keys and codes plain.

On a server-rendered page, highlight on the server. The page then shows the colors without JavaScript:

```tsx
import elanorTheme from '@earendil-works/elanor-code-themes/shiki';
import {CodeSnippet, HighlightedCode} from '@earendil-works/elanor-react';
import {codeToTokensBase} from 'shiki';

const lines = await codeToTokensBase(code, {lang: 'tsx', theme: elanorTheme});

<CodeSnippet>
  <HighlightedCode lines={lines} />
</CodeSnippet>;
```

In the browser, `useHighlightedCode(code, language, highlighter)` highlights after the component mounts. The snippet shows plain code until then, and for languages the highlighter doesn't support. This demo's highlighter uses Shiki and loads each language on first use:

```tsx
// syntax-highlighting.tsx
import {
  CodeSnippet,
  CopyButton,
  HighlightedCode,
  useHighlightedCode,
} from '@earendil-works/elanor-react';

import {highlighter} from './highlighter.ts';

const code = `import {Button} from '@earendil-works/elanor-react';

export function Save({count}: {count: number}) {
  // Disabled until something changes.
  return <Button disabled={count === 0}>Save {count} changes</Button>;
}`;

export default function CodeSnippetSyntaxHighlighting() {
  const lines = useHighlightedCode(code, 'tsx', highlighter);
  return (
    <CodeSnippet actions={<CopyButton value={code} aria-label="Copy code" />}>
      {lines ? <HighlightedCode lines={lines} /> : code}
    </CodeSnippet>
  );
}
```

```tsx
// highlighter.ts
import elanorTheme from '@earendil-works/elanor-code-themes/shiki';
import type {CodeHighlighter} from '@earendil-works/elanor-react';
import type {HighlighterCore, LanguageInput} from 'shiki/core';

// The languages the app highlights, each loaded when it first appears.
const languages = new Map<string, LanguageInput>([
  ['tsx', () => import('shiki/langs/tsx.mjs')],
  ['css', () => import('shiki/langs/css.mjs')],
  ['json', () => import('shiki/langs/json.mjs')],
]);

let shiki: Promise<HighlighterCore> | undefined;

async function createShiki() {
  const [{createHighlighterCore}, {createJavaScriptRegexEngine}] = await Promise.all([
    import('shiki/core'),
    import('shiki/engine/javascript'),
  ]);
  return createHighlighterCore({
    themes: [elanorTheme],
    langs: [],
    engine: createJavaScriptRegexEngine(),
  });
}

export const highlighter: CodeHighlighter = async (code, language) => {
  const grammar = languages.get(language);
  if (!grammar) {
    return null;
  }
  shiki ??= createShiki();
  const loaded = await shiki;
  await loaded.loadLanguage(grammar);
  // A long minified line would stall the page while Shiki tokenizes it; keep such lines plain.
  return loaded.codeToTokensBase(code, {
    lang: language,
    theme: elanorTheme.name,
    tokenizeMaxLineLength: 1000,
  });
};
```

A highlighter using Highlights:

```ts
import elanorTheme from '@earendil-works/elanor-code-themes/highlights';
import type {CodeHighlighter} from '@earendil-works/elanor-react';

export const highlighter: CodeHighlighter = async (code, language) => {
  // Loads Highlights when code first needs it.
  const {codeToTokens, isSupportedLanguage} = await import('@pierre/highlights');
  return isSupportedLanguage(language)
    ? codeToTokens(code, {lang: language, theme: elanorTheme, tokenizeMaxLineLength: 1000}).tokens
    : null;
};
```

Highlights needs WebAssembly with SIMD. Under a Content Security Policy, allow `'wasm-unsafe-eval'`.

## API

### `CodeSnippet`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | - | The code, rendered inside `<code>`. |
| `actions` | `ReactNode` | - | Controls at the end of the snippet, such as a copy button. |

Renders `<pre>`. Also accepts its HTML attributes and `ref`.

### `HighlightedCode`

Renders highlighted lines inside a `CodeSnippet`, on the server or in the browser.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `lines` (required) | `HighlightedLines` | - |  |

Also accepts HTML attributes and `ref`.
