CodeBlock.tsx 2.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
  1. import { useMemo, useState } from 'react';
  2. import { message } from 'antd';
  3. import { CheckOutlined, CopyOutlined } from '@ant-design/icons';
  4. import { ClipboardManager } from '@/utils';
  5. import './CodeBlock.css';
  6. interface CodeBlockProps {
  7. code?: string;
  8. lang?: string;
  9. }
  10. function escapeHtml(str: string): string {
  11. return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
  12. }
  13. function highlightJson(str: string): string {
  14. const escaped = escapeHtml(str);
  15. return escaped.replace(
  16. /("(?:[^"\\]|\\.)*")\s*(:)|("(?:[^"\\]|\\.)*")|(-?\d+\.?\d*(?:[eE][+-]?\d+)?)\b|(true|false)|(null)|([{}[\]])/g,
  17. (_m, key, colon, string, number, bool, nil) => {
  18. if (colon) return `<span class="json-key">${key}</span>${colon}`;
  19. if (string) return `<span class="json-string">${string}</span>`;
  20. if (number) return `<span class="json-number">${number}</span>`;
  21. if (bool) return `<span class="json-boolean">${bool}</span>`;
  22. if (nil) return `<span class="json-null">${nil}</span>`;
  23. return _m;
  24. },
  25. );
  26. }
  27. export default function CodeBlock({ code = '', lang = 'json' }: CodeBlockProps) {
  28. const [copied, setCopied] = useState(false);
  29. const [messageApi, messageContextHolder] = message.useMessage();
  30. const highlighted = useMemo(
  31. () => (lang === 'json' ? highlightJson(code) : escapeHtml(code)),
  32. [code, lang],
  33. );
  34. async function copyCode() {
  35. const ok = await ClipboardManager.copyText(code);
  36. if (ok) {
  37. setCopied(true);
  38. messageApi.success('Copied');
  39. window.setTimeout(() => setCopied(false), 2000);
  40. } else {
  41. messageApi.error('Copy failed');
  42. }
  43. }
  44. return (
  45. <div className="code-block-wrapper">
  46. {messageContextHolder}
  47. <div className="code-toolbar">
  48. <span className="lang-badge">{lang.toUpperCase()}</span>
  49. <button
  50. className={`copy-btn${copied ? ' copied' : ''}`}
  51. onClick={copyCode}
  52. title={copied ? 'Copied' : 'Copy'}
  53. >
  54. {copied ? <CheckOutlined /> : <CopyOutlined />}
  55. </button>
  56. </div>
  57. <pre className={`code-block lang-${lang}`}>
  58. <code dangerouslySetInnerHTML={{ __html: highlighted }} />
  59. </pre>
  60. </div>
  61. );
  62. }