code.tsx 4.0 KB

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