--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import styled from '@emotion/styled';
+import classNames from 'classnames';
+import tw from 'twin.macro';
+import { themeBorder, themeColor } from '../helpers/theme';
+import { isDefined } from '../helpers/types';
+import { CodeSyntaxHighlighter } from './CodeSyntaxHighlighter';
+import { ClipboardButton } from './clipboard';
+
+interface Props {
+ className?: string;
+ isOneLine?: boolean;
+ join?: string;
+ language?: string;
+ noCopy?: boolean;
+ render?: string;
+ snippet: string | Array<string | undefined>;
+ wrap?: boolean;
+}
+
+// keep this "useless" concatenation for the readability reason
+// eslint-disable-next-line no-useless-concat
+const s = ' \\' + '\n ';
+
+export function CodeSnippet(props: Props) {
+ const { className, isOneLine, join = s, language, noCopy, render, snippet, wrap } = props;
+ const snippetArray = Array.isArray(snippet) ? snippet.filter(isDefined) : [snippet];
+ const finalSnippet = isOneLine ? snippetArray.join(' ') : snippetArray.join(join);
+
+ const isSimpleOneLine = isOneLine && noCopy;
+
+ const copyButton = isOneLine ? (
+ <StyledSingleLineClipboardButton copyValue={finalSnippet} />
+ ) : (
+ <StyledClipboardButton copyValue={finalSnippet} />
+ );
+
+ return (
+ <Wrapper
+ className={classNames(
+ {
+ 'code-snippet-highlighted-oneline': isOneLine,
+ 'code-snippet-simple-oneline': isSimpleOneLine,
+ },
+ className,
+ 'fs-mask'
+ )}
+ >
+ {!noCopy && copyButton}
+ <CodeSyntaxHighlighter
+ className={classNames({ 'sw-pr-24': !noCopy, 'sw-flex': !noCopy })}
+ htmlAsString={render ?? finalSnippet}
+ language={language}
+ wrap={wrap}
+ />
+ </Wrapper>
+ );
+}
+
+const Wrapper = styled.div`
+ background-color: ${themeColor('codeSnippetBackground')};
+ border: ${themeBorder('default', 'codeSnippetBorder')};
+
+ ${tw`sw-rounded-2`}
+ ${tw`sw-relative`}
+ ${tw`sw-my-2`}
+
+ &.code-snippet-simple-oneline {
+ ${tw`sw-my-0`}
+ ${tw`sw-rounded-1`}
+ }
+`;
+
+const StyledClipboardButton = styled(ClipboardButton)`
+ ${tw`sw-select-none`}
+ ${tw`sw-body-sm`}
+ ${tw`sw-top-6 sw-right-6`}
+ ${tw`sw-absolute`}
+
+ .code-snippet-highlighted-oneline & {
+ ${tw`sw-bottom-2`}
+ }
+`;
+
+const StyledSingleLineClipboardButton = styled(StyledClipboardButton)`
+ ${tw`sw-top-6 sw-bottom-6`}
+`;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { screen } from '@testing-library/react';
+import { HelmetProvider } from 'react-helmet-async';
+import { renderWithContext } from '../../helpers/testUtils';
+import { FCProps } from '../../types/misc';
+import { CodeSnippet } from '../CodeSnippet';
+
+it('should show full size when multiline with no editting', () => {
+ const { container } = setupWithProps();
+ const copyButton = screen.getByRole('button', { name: 'Copy' });
+ expect(copyButton).toHaveStyle('top: 1.5rem');
+ expect(container).toMatchSnapshot();
+});
+
+it('should show reduced size when single line with no editting', () => {
+ const { container } = setupWithProps({ isOneLine: true, snippet: 'foobar' });
+ const copyButton = screen.getByRole('button', { name: 'Copy' });
+ expect(copyButton).toHaveStyle('top: 1.5rem');
+ expect(container).toMatchSnapshot();
+});
+
+function setupWithProps(props: Partial<FCProps<typeof CodeSnippet>> = {}) {
+ return renderWithContext(
+ <HelmetProvider>
+ <CodeSnippet snippet={'foo\nbar'} {...props} />
+ </HelmetProvider>
+ );
+}
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should show full size when multiline with no editting 1`] = `
+.emotion-0 {
+ background-color: rgb(252,252,253);
+ border: 1px solid rgb(225,230,243);
+ border-radius: 0.5rem;
+ position: relative;
+ margin-top: 0.5rem;
+ margin-bottom: 0.5rem;
+}
+
+.emotion-0.code-snippet-simple-oneline {
+ margin-top: 0;
+ margin-bottom: 0;
+ border-radius: 0.25rem;
+}
+
+.emotion-4 {
+ box-sizing: border-box;
+ -webkit-text-decoration: none;
+ text-decoration: none;
+ outline: none;
+ border: var(--border);
+ color: var(--color);
+ background-color: var(--background);
+ -webkit-transition: background-color 0.2s ease,outline 0.2s ease;
+ transition: background-color 0.2s ease,outline 0.2s ease;
+ display: -webkit-inline-box;
+ display: -webkit-inline-flex;
+ display: -ms-inline-flexbox;
+ display: inline-flex;
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ height: 2.25rem;
+ font-family: Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ font-weight: 600;
+ padding-left: 1rem;
+ padding-right: 1rem;
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+ border-radius: 0.5rem;
+ cursor: pointer;
+ --background: rgb(255,255,255);
+ --backgroundHover: rgb(239,242,249);
+ --color: rgb(62,67,87);
+ --focus: rgba(197,205,223,0.2);
+ --border: 1px solid rgb(197,205,223);
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ font-family: Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ font-weight: 400;
+ right: 1.5rem;
+ top: 1.5rem;
+ position: absolute;
+}
+
+.emotion-4:hover {
+ color: var(--color);
+ background-color: var(--backgroundHover);
+}
+
+.emotion-4:focus,
+.emotion-4:active {
+ color: var(--color);
+ outline: 4px solid var(--focus);
+}
+
+.emotion-4:disabled,
+.emotion-4:disabled:hover {
+ color: rgb(166,173,194);
+ background-color: rgb(239,242,249);
+ border: 1px solid rgb(197,205,223);
+ cursor: not-allowed;
+}
+
+.emotion-4>svg {
+ margin-right: 0.25rem;
+}
+
+.emotion-4 [disabled] {
+ pointer-events: none;
+}
+
+.code-snippet-highlighted-oneline .emotion-4 {
+ bottom: 0.5rem;
+}
+
+.emotion-6 code {
+ font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
+ font-size: 0.875rem;
+ line-height: 1.125rem;
+ font-weight: 400;
+ background: rgb(252,252,253);
+ color: rgb(51,53,60);
+}
+
+.emotion-6 code.hljs {
+ padding: unset;
+}
+
+.emotion-6 .hljs-meta,
+.emotion-6 .hljs-variable {
+ color: rgb(51,53,60);
+}
+
+.emotion-6 .hljs-doctag,
+.emotion-6 .hljs-title,
+.emotion-6 .hljs-title.class_,
+.emotion-6 .hljs-title.function_ {
+ color: rgb(34,84,192);
+}
+
+.emotion-6 .hljs-comment {
+ font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
+ font-size: 0.875rem;
+ line-height: 1.125rem;
+ font-style: italic;
+ color: rgb(109,111,119);
+}
+
+.emotion-6 .hljs-keyword,
+.emotion-6 .hljs-tag,
+.emotion-6 .hljs-type {
+ color: rgb(152,29,150);
+}
+
+.emotion-6 .hljs-literal,
+.emotion-6 .hljs-number {
+ color: rgb(126,83,5);
+}
+
+.emotion-6 .hljs-string {
+ color: rgb(32,105,31);
+}
+
+.emotion-6 .hljs-meta .hljs-keyword {
+ color: rgb(47,103,48);
+}
+
+.emotion-6.code-wrap {
+ white-space: pre-wrap;
+ word-break: break-all;
+}
+
+.emotion-6 mark {
+ font-weight: 400;
+ padding: 0.25rem;
+ border-radius: 0.25rem;
+ background-color: rgb(197,205,223);
+ color: rgb(217,45,32);
+}
+
+<div>
+ <div
+ class="fs-mask emotion-0 emotion-1"
+ >
+ <button
+ aria-describedby="tooltip-1"
+ class="sw-select-none emotion-2 emotion-3 emotion-4 emotion-5"
+ data-clipboard-text="foo
+bar"
+ type="button"
+ >
+ <svg
+ aria-hidden="true"
+ class="octicon octicon-copy"
+ fill="currentColor"
+ focusable="false"
+ height="16"
+ role="img"
+ style="display: inline-block; user-select: none; vertical-align: middle; overflow: visible;"
+ viewBox="0 0 16 16"
+ width="16"
+ >
+ <path
+ d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"
+ />
+ <path
+ d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"
+ />
+ </svg>
+ Copy
+ </button>
+ <span
+ class="hljs sw-pr-24 sw-flex emotion-6 emotion-7"
+ >
+ foo
+bar
+ </span>
+ </div>
+</div>
+`;
+
+exports[`should show reduced size when single line with no editting 1`] = `
+.emotion-0 {
+ background-color: rgb(252,252,253);
+ border: 1px solid rgb(225,230,243);
+ border-radius: 0.5rem;
+ position: relative;
+ margin-top: 0.5rem;
+ margin-bottom: 0.5rem;
+}
+
+.emotion-0.code-snippet-simple-oneline {
+ margin-top: 0;
+ margin-bottom: 0;
+ border-radius: 0.25rem;
+}
+
+.emotion-4 {
+ box-sizing: border-box;
+ -webkit-text-decoration: none;
+ text-decoration: none;
+ outline: none;
+ border: var(--border);
+ color: var(--color);
+ background-color: var(--background);
+ -webkit-transition: background-color 0.2s ease,outline 0.2s ease;
+ transition: background-color 0.2s ease,outline 0.2s ease;
+ display: -webkit-inline-box;
+ display: -webkit-inline-flex;
+ display: -ms-inline-flexbox;
+ display: inline-flex;
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ height: 2.25rem;
+ font-family: Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ font-weight: 600;
+ padding-left: 1rem;
+ padding-right: 1rem;
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+ border-radius: 0.5rem;
+ cursor: pointer;
+ --background: rgb(255,255,255);
+ --backgroundHover: rgb(239,242,249);
+ --color: rgb(62,67,87);
+ --focus: rgba(197,205,223,0.2);
+ --border: 1px solid rgb(197,205,223);
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ font-family: Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ font-weight: 400;
+ right: 1.5rem;
+ top: 1.5rem;
+ position: absolute;
+ bottom: 1.5rem;
+ top: 1.5rem;
+}
+
+.emotion-4:hover {
+ color: var(--color);
+ background-color: var(--backgroundHover);
+}
+
+.emotion-4:focus,
+.emotion-4:active {
+ color: var(--color);
+ outline: 4px solid var(--focus);
+}
+
+.emotion-4:disabled,
+.emotion-4:disabled:hover {
+ color: rgb(166,173,194);
+ background-color: rgb(239,242,249);
+ border: 1px solid rgb(197,205,223);
+ cursor: not-allowed;
+}
+
+.emotion-4>svg {
+ margin-right: 0.25rem;
+}
+
+.emotion-4 [disabled] {
+ pointer-events: none;
+}
+
+.code-snippet-highlighted-oneline .emotion-4 {
+ bottom: 0.5rem;
+}
+
+.emotion-6 code {
+ font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
+ font-size: 0.875rem;
+ line-height: 1.125rem;
+ font-weight: 400;
+ background: rgb(252,252,253);
+ color: rgb(51,53,60);
+}
+
+.emotion-6 code.hljs {
+ padding: unset;
+}
+
+.emotion-6 .hljs-meta,
+.emotion-6 .hljs-variable {
+ color: rgb(51,53,60);
+}
+
+.emotion-6 .hljs-doctag,
+.emotion-6 .hljs-title,
+.emotion-6 .hljs-title.class_,
+.emotion-6 .hljs-title.function_ {
+ color: rgb(34,84,192);
+}
+
+.emotion-6 .hljs-comment {
+ font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
+ font-size: 0.875rem;
+ line-height: 1.125rem;
+ font-style: italic;
+ color: rgb(109,111,119);
+}
+
+.emotion-6 .hljs-keyword,
+.emotion-6 .hljs-tag,
+.emotion-6 .hljs-type {
+ color: rgb(152,29,150);
+}
+
+.emotion-6 .hljs-literal,
+.emotion-6 .hljs-number {
+ color: rgb(126,83,5);
+}
+
+.emotion-6 .hljs-string {
+ color: rgb(32,105,31);
+}
+
+.emotion-6 .hljs-meta .hljs-keyword {
+ color: rgb(47,103,48);
+}
+
+.emotion-6.code-wrap {
+ white-space: pre-wrap;
+ word-break: break-all;
+}
+
+.emotion-6 mark {
+ font-weight: 400;
+ padding: 0.25rem;
+ border-radius: 0.25rem;
+ background-color: rgb(197,205,223);
+ color: rgb(217,45,32);
+}
+
+<div>
+ <div
+ class="code-snippet-highlighted-oneline fs-mask emotion-0 emotion-1"
+ >
+ <button
+ aria-describedby="tooltip-2"
+ class="sw-select-none emotion-2 emotion-3 emotion-4 emotion-5"
+ data-clipboard-text="foobar"
+ type="button"
+ >
+ <svg
+ aria-hidden="true"
+ class="octicon octicon-copy"
+ fill="currentColor"
+ focusable="false"
+ height="16"
+ role="img"
+ style="display: inline-block; user-select: none; vertical-align: middle; overflow: visible;"
+ viewBox="0 0 16 16"
+ width="16"
+ >
+ <path
+ d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"
+ />
+ <path
+ d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"
+ />
+ </svg>
+ Copy
+ </button>
+ <span
+ class="hljs sw-pr-24 sw-flex emotion-6 emotion-7"
+ >
+ foobar
+ </span>
+ </div>
+</div>
+`;