mattrguerra/cortexmainPrivate on GitHub, published here in full

Markdown.tsx

Language TypeScript3.2 KB111 lines

src/components/Markdown.tsx

  1. import { memo, useState } from 'react'
  2. import ReactMarkdown from 'react-markdown'
  3. import rehypeHighlight from 'rehype-highlight'
  4. import remarkGfm from 'remark-gfm'
  5. import bash from 'highlight.js/lib/languages/bash'
  6. import css from 'highlight.js/lib/languages/css'
  7. import diff from 'highlight.js/lib/languages/diff'
  8. import go from 'highlight.js/lib/languages/go'
  9. import javascript from 'highlight.js/lib/languages/javascript'
  10. import json from 'highlight.js/lib/languages/json'
  11. import markdown from 'highlight.js/lib/languages/markdown'
  12. import python from 'highlight.js/lib/languages/python'
  13. import rust from 'highlight.js/lib/languages/rust'
  14. import sql from 'highlight.js/lib/languages/sql'
  15. import typescript from 'highlight.js/lib/languages/typescript'
  16. import xml from 'highlight.js/lib/languages/xml'
  17. import yaml from 'highlight.js/lib/languages/yaml'
  18. import { useStore } from '../state/store'
  19. const languages = {
  20. bash,
  21. sh: bash,
  22. shell: bash,
  23. css,
  24. diff,
  25. go,
  26. javascript,
  27. js: javascript,
  28. json,
  29. markdown,
  30. md: markdown,
  31. python,
  32. py: python,
  33. rust,
  34. rs: rust,
  35. sql,
  36. typescript,
  37. ts: typescript,
  38. tsx: typescript,
  39. xml,
  40. html: xml,
  41. yaml,
  42. yml: yaml,
  43. }
  44. function textOf(node: unknown): string {
  45. if (typeof node === 'string') return node
  46. if (Array.isArray(node)) return node.map(textOf).join('')
  47. if (node && typeof node === 'object' && 'props' in node)
  48. return textOf((node as { props: { children?: unknown } }).props.children)
  49. return ''
  50. }
  51. function CodeBlock({ children }: { children?: React.ReactNode }) {
  52. const [copied, setCopied] = useState(false)
  53. const code = textOf(children).replace(/\n$/, '')
  54. const lang = /language-(\w+)/.exec(
  55. ((children as { props?: { className?: string } })?.props?.className ?? '') as string,
  56. )?.[1]
  57. return (
  58. <div className="codeblock">
  59. <div className="codeblock-bar">
  60. <span>{lang ?? 'text'}</span>
  61. <button
  62. className="ghost small"
  63. onClick={() => {
  64. void navigator.clipboard?.writeText(code)
  65. setCopied(true)
  66. setTimeout(() => setCopied(false), 1400)
  67. }}
  68. >
  69. {copied ? 'Copied' : 'Copy'}
  70. </button>
  71. </div>
  72. <pre>{children}</pre>
  73. </div>
  74. )
  75. }
  76. /** Assistant text. No raw HTML; links open in the browser, https only. */
  77. export const Markdown = memo(function Markdown({ text }: { text: string }) {
  78. const client = useStore((s) => s.client)
  79. return (
  80. <div className="md selectable">
  81. <ReactMarkdown
  82. remarkPlugins={[remarkGfm]}
  83. rehypePlugins={[[rehypeHighlight, { languages, detect: false, ignoreMissing: true }]]}
  84. skipHtml
  85. components={{
  86. pre: ({ children }) => <CodeBlock>{children}</CodeBlock>,
  87. a: ({ href, children }) => (
  88. <a
  89. href={href}
  90. onClick={(e) => {
  91. e.preventDefault()
  92. if (href && /^https:\/\//.test(href)) void client?.openUrl(href)
  93. }}
  94. title={href}
  95. >
  96. {children}
  97. </a>
  98. ),
  99. img: ({ alt }) => <span className="md-img">[image: {alt || 'untitled'}]</span>,
  100. }}
  101. >
  102. {text}
  103. </ReactMarkdown>
  104. </div>
  105. )
  106. })