code.tsx 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. // eslint-disable-next-line simple-import-sort/imports
  2. import 'prismjs/themes/prism.css';
  3. import {useEffect, useRef, useState} from 'react';
  4. import {useTheme, Theme} from '@emotion/react';
  5. import styled from '@emotion/styled';
  6. import copy from 'copy-text-to-clipboard';
  7. import Prism from 'prismjs';
  8. /**
  9. * JSX syntax for Prism. This file uses Prism
  10. * internally, so it must be imported after Prism.
  11. */
  12. import 'prismjs/components/prism-jsx.min';
  13. import {IconCode} from 'sentry/icons';
  14. import space from 'sentry/styles/space';
  15. type Props = {
  16. /**
  17. * Main code content gets passed as the children prop
  18. */
  19. children: string;
  20. /**
  21. * Auto-generated class name for <pre> and <code> element,
  22. * with a 'language-' prefix, e.g. language-css
  23. */
  24. className?: string;
  25. /**
  26. * Meta props from the markdown syntax,
  27. * for example, in
  28. *
  29. * ```jsx label=hello
  30. * [some code]
  31. * ```
  32. *
  33. * the label prop is set to 'hello'
  34. */
  35. label?: string;
  36. theme?: Theme;
  37. };
  38. const Code = ({children, className, label}: Props) => {
  39. const theme = useTheme();
  40. const codeRef = useRef<HTMLElement | null>(null);
  41. const copyTimeoutRef = useRef<number | undefined>(undefined);
  42. const [copied, setCopied] = useState(false);
  43. function handleCopyCode() {
  44. // Remove comments from code
  45. const copiableContent = children.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '');
  46. copy(copiableContent);
  47. setCopied(true);
  48. copyTimeoutRef.current = window.setTimeout(() => {
  49. setCopied(false);
  50. }, 500);
  51. }
  52. // Cleanup timeout on component unmount
  53. useEffect(() => {
  54. return () => {
  55. if (copyTimeoutRef.current !== null) {
  56. window.clearTimeout(copyTimeoutRef.current);
  57. }
  58. };
  59. }, []);
  60. useEffect(() => {
  61. Prism.highlightElement(codeRef.current, false);
  62. }, [children]);
  63. return (
  64. <Wrap className={className}>
  65. <LabelWrap>
  66. <IconCode theme={theme} color="subText" />
  67. {label && <Label>{label.replaceAll('_', ' ')}</Label>}
  68. </LabelWrap>
  69. <HighlightedCode className={className} ref={codeRef}>
  70. {children}
  71. </HighlightedCode>
  72. <CopyButton onClick={handleCopyCode} disabled={copied}>
  73. {copied ? 'Copied' : 'Copy'}
  74. </CopyButton>
  75. </Wrap>
  76. );
  77. };
  78. export default Code;
  79. const Wrap = styled('pre')`
  80. /* Increase specificity to override default styles */
  81. && {
  82. position: relative;
  83. padding: ${space(2)};
  84. padding-top: ${space(4)};
  85. margin-top: ${space(4)};
  86. margin-bottom: ${space(2)};
  87. background: ${p => p.theme.bodyBackground};
  88. border: solid 1px ${p => p.theme.border};
  89. overflow: visible;
  90. text-shadow: none;
  91. }
  92. & code {
  93. text-shadow: none;
  94. }
  95. /* Overwrite default Prism behavior to allow for code wrapping */
  96. pre[class*='language-'],
  97. code[class*='language-'] {
  98. white-space: normal;
  99. word-break: break-word;
  100. }
  101. `;
  102. const LabelWrap = styled('div')`
  103. display: flex;
  104. align-items: center;
  105. position: absolute;
  106. top: 0;
  107. left: calc(${space(2)} - ${space(1)});
  108. transform: translateY(-50%);
  109. padding: ${space(0.25)} ${space(1)};
  110. background: ${p => p.theme.docsBackground};
  111. border: solid 1px ${p => p.theme.border};
  112. border-radius: ${p => p.theme.borderRadius};
  113. `;
  114. const Label = styled('p')`
  115. font-size: 0.875rem;
  116. font-weight: 600;
  117. color: ${p => p.theme.subText};
  118. text-transform: uppercase;
  119. margin-bottom: 0;
  120. margin-left: ${space(1)};
  121. `;
  122. const HighlightedCode = styled('code')`
  123. /** Increase specificity to override default styles */
  124. ${Wrap} > & {
  125. font-family: ${p => p.theme.text.familyMono};
  126. font-size: 0.875rem;
  127. line-height: 1.6;
  128. }
  129. `;
  130. const CopyButton = styled('button')`
  131. position: absolute;
  132. top: ${space(0.5)};
  133. right: ${space(0.5)};
  134. background: transparent;
  135. border: none;
  136. border-radius: ${p => p.theme.borderRadius};
  137. padding: ${space(0.5)} ${space(1)};
  138. font-size: 0.875rem;
  139. font-weight: 600;
  140. color: ${p => p.theme.subText};
  141. &:hover:not(:disabled) {
  142. color: ${p => p.theme.textColor};
  143. }
  144. `;