CodeBlock.tsx 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. import Prism from "prismjs"
  2. import * as React from "react"
  3. import { Codicon } from "./Codicon"
  4. let mermaidInitialized = false
  5. function MermaidBlock({ children }) {
  6. const ref = React.useRef(null)
  7. const [svg, setSvg] = React.useState("")
  8. const [error, setError] = React.useState(false)
  9. React.useEffect(() => {
  10. const code = typeof children === "string" ? children : ref.current?.textContent || ""
  11. if (!code.trim()) return
  12. const id = `mermaid-${Math.random().toString(36).slice(2, 9)}`
  13. import("mermaid").then((mod) => {
  14. const mermaid = mod.default
  15. if (!mermaidInitialized) {
  16. mermaid.initialize({
  17. startOnLoad: false,
  18. theme: "base",
  19. themeVariables: {
  20. primaryColor: "#33332d",
  21. primaryTextColor: "#e9e9e9",
  22. primaryBorderColor: "#555",
  23. lineColor: "#a3a3a2",
  24. secondaryColor: "#2a2a24",
  25. tertiaryColor: "#1a1a18",
  26. background: "#1a1a18",
  27. mainBkg: "#33332d",
  28. nodeBorder: "#555",
  29. clusterBkg: "#2a2a24",
  30. clusterBorder: "#444",
  31. titleColor: "#e9e9e9",
  32. edgeLabelBackground: "#1a1a18",
  33. },
  34. securityLevel: "strict",
  35. fontFamily: "inherit",
  36. })
  37. mermaidInitialized = true
  38. }
  39. mermaid
  40. .render(id, code.trim())
  41. .then(({ svg }) => setSvg(svg))
  42. .catch((err) => {
  43. console.error(err)
  44. setError(true)
  45. })
  46. })
  47. }, [children])
  48. if (error) {
  49. return <pre className="language-mermaid">{children}</pre>
  50. }
  51. if (svg) {
  52. return (
  53. <div
  54. className="mermaid-diagram"
  55. dangerouslySetInnerHTML={{ __html: svg }}
  56. style={{ display: "flex", justifyContent: "center", padding: "1rem 0" }}
  57. />
  58. )
  59. }
  60. return (
  61. <pre ref={ref} style={{ visibility: "hidden", height: 0, overflow: "hidden" }}>
  62. {children}
  63. </pre>
  64. )
  65. }
  66. export function CodeBlock({ children, "data-language": language }) {
  67. const ref = React.useRef(null)
  68. const timeoutRef = React.useRef(null)
  69. const [copied, setCopied] = React.useState(false)
  70. React.useEffect(() => {
  71. if (language === "mermaid") return
  72. if (ref.current) Prism.highlightElement(ref.current, false)
  73. }, [children, language])
  74. React.useEffect(() => {
  75. return () => {
  76. if (timeoutRef.current) {
  77. clearTimeout(timeoutRef.current)
  78. }
  79. }
  80. }, [])
  81. if (language === "mermaid") {
  82. return <MermaidBlock>{children}</MermaidBlock>
  83. }
  84. const handleCopy = async () => {
  85. const code = ref.current?.textContent || ""
  86. try {
  87. await navigator.clipboard.writeText(code)
  88. setCopied(true)
  89. if (timeoutRef.current) {
  90. clearTimeout(timeoutRef.current)
  91. }
  92. timeoutRef.current = setTimeout(() => setCopied(false), 2000)
  93. } catch (err) {
  94. console.error("Failed to copy code:", err)
  95. }
  96. }
  97. return (
  98. <div className="code" aria-live="polite">
  99. <button
  100. type="button"
  101. className="copy-button"
  102. onClick={handleCopy}
  103. aria-label="Copy code to clipboard"
  104. title={copied ? "Copied!" : "Copy code"}
  105. >
  106. {copied ? <Codicon name="check" /> : <Codicon name="copy" />}
  107. </button>
  108. <pre ref={ref} className={`language-${language}`}>
  109. {children}
  110. </pre>
  111. <style jsx>
  112. {`
  113. .code {
  114. position: relative;
  115. }
  116. .copy-button {
  117. position: absolute;
  118. top: 8px;
  119. right: 8px;
  120. padding: 6px 8px;
  121. background: rgba(0, 0, 0, 0.05);
  122. border: 1px solid rgba(0, 0, 0, 0.1);
  123. border-radius: 4px;
  124. color: rgba(0, 0, 0, 0.4);
  125. cursor: pointer;
  126. display: flex;
  127. align-items: center;
  128. justify-content: center;
  129. transition: all 0.2s ease;
  130. z-index: 10;
  131. }
  132. .copy-button:hover {
  133. background: rgba(0, 0, 0, 0.1);
  134. color: rgba(0, 0, 0, 0.6);
  135. border-color: rgba(0, 0, 0, 0.2);
  136. }
  137. :global(.dark) .copy-button {
  138. background: rgba(255, 255, 255, 0.05);
  139. border-color: rgba(255, 255, 255, 0.1);
  140. color: rgba(255, 255, 255, 0.4);
  141. }
  142. :global(.dark) .copy-button:hover {
  143. background: rgba(255, 255, 255, 0.1);
  144. color: rgba(255, 255, 255, 0.7);
  145. border-color: rgba(255, 255, 255, 0.2);
  146. }
  147. .copy-button:active {
  148. transform: scale(0.95);
  149. }
  150. /* Override Prism styles */
  151. .code :global(pre[class*="language-"]) {
  152. text-shadow: none;
  153. border-radius: 4px;
  154. padding-right: 3.5rem;
  155. }
  156. `}
  157. </style>
  158. </div>
  159. )
  160. }