initial
This commit is contained in:
77
apps/web/src/components/CodeBlock.tsx
Normal file
77
apps/web/src/components/CodeBlock.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { useState } from 'react';
|
||||
import { Check, Copy } from 'lucide-react';
|
||||
|
||||
// NOTE: spec calls for syntax-highlighted code blocks. Highlighting deferred
|
||||
// to keep dep footprint minimal; this renders styled mono code with a copy
|
||||
// button. Adding a highlighter (shiki / highlight.js) is a one-import swap.
|
||||
interface Props {
|
||||
code: string;
|
||||
lang?: string;
|
||||
}
|
||||
|
||||
export function CodeBlock({ code, lang }: Props) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
async function copy() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(code);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1200);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-md border bg-muted/40 overflow-hidden text-sm my-1">
|
||||
<div className="flex items-center justify-between px-2 py-1 border-b text-xs text-muted-foreground">
|
||||
<span className="font-mono">{lang || 'code'}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void copy()}
|
||||
className="flex items-center gap-1 px-1.5 py-0.5 rounded hover:bg-muted text-foreground"
|
||||
aria-label="Copy code"
|
||||
>
|
||||
{copied ? <Check className="size-3" /> : <Copy className="size-3" />}
|
||||
<span>{copied ? 'Copied' : 'Copy'}</span>
|
||||
</button>
|
||||
</div>
|
||||
<pre className="overflow-x-auto px-3 py-2 font-mono text-xs leading-relaxed">
|
||||
{code}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface SegmentText {
|
||||
kind: 'text';
|
||||
value: string;
|
||||
}
|
||||
interface SegmentCode {
|
||||
kind: 'code';
|
||||
lang?: string;
|
||||
value: string;
|
||||
}
|
||||
export type Segment = SegmentText | SegmentCode;
|
||||
|
||||
export function splitCodeBlocks(input: string): Segment[] {
|
||||
const segments: Segment[] = [];
|
||||
const fence = /```([a-zA-Z0-9_-]*)\n([\s\S]*?)```/g;
|
||||
let lastIndex = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = fence.exec(input)) !== null) {
|
||||
if (match.index > lastIndex) {
|
||||
segments.push({ kind: 'text', value: input.slice(lastIndex, match.index) });
|
||||
}
|
||||
segments.push({
|
||||
kind: 'code',
|
||||
lang: match[1] || undefined,
|
||||
value: (match[2] ?? '').replace(/\n$/, ''),
|
||||
});
|
||||
lastIndex = match.index + match[0].length;
|
||||
}
|
||||
if (lastIndex < input.length) {
|
||||
segments.push({ kind: 'text', value: input.slice(lastIndex) });
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
Reference in New Issue
Block a user