code.tsx 3.7 KB

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