diff options
author | Kevin Silva <kevin.silva@sonarsource.com> | 2023-05-17 12:17:17 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-05-17 20:02:41 +0000 |
commit | 814e30e422183726861cb4342c8e67d28ff9015d (patch) | |
tree | b857ed273b3c44a2391fe8dd4b80688cca6bd54f /server | |
parent | 8e10ac9ab50a1e2a0f40ba1ad07907a8017a02df (diff) | |
download | sonarqube-814e30e422183726861cb4342c8e67d28ff9015d.tar.gz sonarqube-814e30e422183726861cb4342c8e67d28ff9015d.zip |
SONAR-19173 - Create a "code snippet" component design-system
Diffstat (limited to 'server')
12 files changed, 1386 insertions, 0 deletions
diff --git a/server/sonar-web/design-system/package.json b/server/sonar-web/design-system/package.json index 45633e20409..ff65cf5ab5b 100644 --- a/server/sonar-web/design-system/package.json +++ b/server/sonar-web/design-system/package.json @@ -34,6 +34,7 @@ "eslint-plugin-import": "2.27.5", "eslint-plugin-local-rules": "1.3.2", "eslint-plugin-typescript-sort-keys": "2.3.0", + "highlight.js": "11.7.0", "history": "5.3.0", "jest": "29.5.0", "postcss": "8.4.21", diff --git a/server/sonar-web/design-system/src/components/CodeSnippet.tsx b/server/sonar-web/design-system/src/components/CodeSnippet.tsx new file mode 100644 index 00000000000..e1fed4f8808 --- /dev/null +++ b/server/sonar-web/design-system/src/components/CodeSnippet.tsx @@ -0,0 +1,120 @@ +/* + * 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 { Highlighter, RegisteredLanguages } from './Highlighter'; +import { ClipboardButton } from './clipboard'; + +interface Props { + className?: string; + highlight?: boolean; + isOneLine?: boolean; + join?: string; + language?: RegisteredLanguages; + noCopy?: boolean; + render?: string; + snippet: string | Array<string | undefined>; + toggleEdit?: VoidFunction; + 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, + highlight, + join = s, + language, + noCopy, + render, + snippet, + toggleEdit, + 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} + <Highlighter + code={render ?? finalSnippet} + highlight={highlight} + isSimpleOneLine={isSimpleOneLine} + language={language} + toggleEdit={isOneLine ? toggleEdit : undefined} + 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`} +`; diff --git a/server/sonar-web/design-system/src/components/Highlighter.tsx b/server/sonar-web/design-system/src/components/Highlighter.tsx new file mode 100644 index 00000000000..73690313334 --- /dev/null +++ b/server/sonar-web/design-system/src/components/Highlighter.tsx @@ -0,0 +1,182 @@ +/* + * 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 hljs from 'highlight.js/lib/core'; +import bash from 'highlight.js/lib/languages/bash'; +import gradle from 'highlight.js/lib/languages/gradle'; +import plaintext from 'highlight.js/lib/languages/plaintext'; +import powershell from 'highlight.js/lib/languages/powershell'; +import properties from 'highlight.js/lib/languages/properties'; +import shell from 'highlight.js/lib/languages/shell'; +import xml from 'highlight.js/lib/languages/xml'; +import yaml from 'highlight.js/lib/languages/yaml'; +import { useMemo } from 'react'; +import tw from 'twin.macro'; +import { translate } from '../helpers/l10n'; +import { themeColor, themeContrast } from '../helpers/theme'; +import { InteractiveIcon } from './InteractiveIcon'; +import { PencilIcon } from './icons'; + +hljs.registerLanguage('yaml', yaml); +hljs.registerLanguage('gradle', gradle); +hljs.registerLanguage('properties', properties); +hljs.registerLanguage('xml', xml); +hljs.registerLanguage('bash', bash); +hljs.registerLanguage('powershell', powershell); +hljs.registerLanguage('shell', shell); +hljs.registerLanguage('plaintext', plaintext); + +hljs.addPlugin({ + 'after:highlight': (data) => { + data.value = data.value + .replace(/<mark>/g, '<mark>') + .replace(/<\/mark>/g, '</mark>'); + }, +}); + +export type RegisteredLanguages = + | 'bash' + | 'gradle' + | 'plaintext' + | 'powershell' + | 'properties' + | 'shell' + | 'xml' + | 'yaml'; + +interface Props { + className?: string; + code: string; + highlight?: boolean; + isSimpleOneLine?: boolean; + language?: RegisteredLanguages; + toggleEdit?: VoidFunction; + wrap?: boolean; +} + +export function Highlighter({ + className, + code, + highlight = true, + isSimpleOneLine = false, + language = 'yaml', + toggleEdit, + wrap, +}: Props) { + const highlighted = useMemo( + () => hljs.highlight(code, { language: highlight ? language : 'plaintext' }), + [code, highlight, language] + ); + + return ( + <StyledPre + className={classNames({ 'code-wrap': wrap, 'simple-one-line': isSimpleOneLine }, className)} + > + <code + className={classNames('hljs', { 'sw-inline': toggleEdit })} + // Safe: value is escaped by highlight.js + // eslint-disable-next-line react/no-danger + dangerouslySetInnerHTML={{ __html: highlighted.value }} + /> + {toggleEdit && ( + <InteractiveIcon + Icon={PencilIcon} + aria-label={translate('edit')} + className="sw-ml-2" + onClick={toggleEdit} + /> + )} + </StyledPre> + ); +} + +const StyledPre = styled.pre` + ${tw`sw-flex sw-items-center`} + ${tw`sw-overflow-x-auto`} + ${tw`sw-p-6`} + + code { + color: ${themeColor('codeSnippetBody')}; + background: ${themeColor('codeSnippetBackground')}; + ${tw`sw-code`}; + + &.hljs { + padding: unset; + } + } + + .hljs-variable, + .hljs-meta { + color: ${themeColor('codeSnippetBody')}; + } + + .hljs-doctag, + .hljs-title, + .hljs-title.class_, + .hljs-title.function_ { + color: ${themeColor('codeSnippetAnnotations')}; + } + + .hljs-comment { + color: ${themeColor('codeSnippetComments')}; + + ${tw`sw-code-comment`} + } + + .hljs-tag, + .hljs-type, + .hljs-keyword { + color: ${themeColor('codeSnippetKeyword')}; + + ${tw`sw-code-highlight`} + } + + .hljs-literal, + .hljs-number { + color: ${themeColor('codeSnippetConstants')}; + } + + .hljs-string { + color: ${themeColor('codeSnippetString')}; + } + + .hljs-meta .hljs-keyword { + color: ${themeColor('codeSnippetPreprocessingDirective')}; + } + + &.code-wrap { + ${tw`sw-whitespace-pre-wrap`} + ${tw`sw-break-all`} + } + + mark { + color: ${themeContrast('codeSnippetHighlight')}; + background-color: ${themeColor('codeSnippetHighlight')}; + ${tw`sw-font-regular`} + ${tw`sw-rounded-1`} + ${tw`sw-p-1`} + } + + &.simple-one-line { + ${tw`sw-min-h-[1.25rem]`} + ${tw`sw-py-0 sw-px-1`} + } +`; diff --git a/server/sonar-web/design-system/src/components/__tests__/CodeSnippet-test.tsx b/server/sonar-web/design-system/src/components/__tests__/CodeSnippet-test.tsx new file mode 100644 index 00000000000..41e98ce716d --- /dev/null +++ b/server/sonar-web/design-system/src/components/__tests__/CodeSnippet-test.tsx @@ -0,0 +1,46 @@ +/* + * 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> + ); +} diff --git a/server/sonar-web/design-system/src/components/__tests__/Highlighter-test.tsx b/server/sonar-web/design-system/src/components/__tests__/Highlighter-test.tsx new file mode 100644 index 00000000000..c02ca7f6e51 --- /dev/null +++ b/server/sonar-web/design-system/src/components/__tests__/Highlighter-test.tsx @@ -0,0 +1,50 @@ +/* + * 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 { renderWithContext } from '../../helpers/testUtils'; +import { FCProps } from '../../types/misc'; +import { Highlighter } from '../Highlighter'; + +it('renders correctly', () => { + expect(setupWithProps().container).toMatchSnapshot(); +}); + +it('should handle multiple lines of code', () => { + expect( + setupWithProps({ + code: `foo: bar + pleh: help + stuff: + foo: bar + bar: foo`, + language: 'yaml', + }).container + ).toMatchSnapshot(); +}); + +it('should display edit functions', () => { + expect( + setupWithProps({ code: 'One line command', toggleEdit: jest.fn() }).container + ).toMatchSnapshot(); +}); + +function setupWithProps(props: Partial<FCProps<typeof Highlighter>> = {}) { + return renderWithContext(<Highlighter code="foo\nbar" {...props} />); +} diff --git a/server/sonar-web/design-system/src/components/__tests__/__snapshots__/CodeSnippet-test.tsx.snap b/server/sonar-web/design-system/src/components/__tests__/__snapshots__/CodeSnippet-test.tsx.snap new file mode 100644 index 00000000000..715c4e4d72d --- /dev/null +++ b/server/sonar-web/design-system/src/components/__tests__/__snapshots__/CodeSnippet-test.tsx.snap @@ -0,0 +1,471 @@ +// 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 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + overflow-x: auto; + padding: 1.5rem; +} + +.emotion-6 code { + color: rgb(51,53,60); + background: rgb(252,252,253); + font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 400; +} + +.emotion-6 code.hljs { + padding: unset; +} + +.emotion-6 .hljs-variable, +.emotion-6 .hljs-meta { + 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 { + color: rgb(109,111,119); + font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; + font-size: 0.875rem; + line-height: 1.25rem; + font-style: italic; +} + +.emotion-6 .hljs-tag, +.emotion-6 .hljs-type, +.emotion-6 .hljs-keyword { + color: rgb(152,29,150); + font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 700; +} + +.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 { + color: rgb(217,45,32); + background-color: rgb(197,205,223); + font-weight: 400; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.emotion-6.simple-one-line { + min-height: 1.25rem; + padding-left: 0.25rem; + padding-right: 0.25rem; + padding-top: 0; + padding-bottom: 0; +} + +<div> + <div + class="fs-mask emotion-0 emotion-1" + > + <button + 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> + <pre + class=" emotion-6 emotion-7" + > + <code + class="hljs" + > + <span + class="hljs-string" + > + foo + </span> + + + <span + class="hljs-string" + > + bar + </span> + </code> + </pre> + </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 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + overflow-x: auto; + padding: 1.5rem; +} + +.emotion-6 code { + color: rgb(51,53,60); + background: rgb(252,252,253); + font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 400; +} + +.emotion-6 code.hljs { + padding: unset; +} + +.emotion-6 .hljs-variable, +.emotion-6 .hljs-meta { + 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 { + color: rgb(109,111,119); + font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; + font-size: 0.875rem; + line-height: 1.25rem; + font-style: italic; +} + +.emotion-6 .hljs-tag, +.emotion-6 .hljs-type, +.emotion-6 .hljs-keyword { + color: rgb(152,29,150); + font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 700; +} + +.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 { + color: rgb(217,45,32); + background-color: rgb(197,205,223); + font-weight: 400; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.emotion-6.simple-one-line { + min-height: 1.25rem; + padding-left: 0.25rem; + padding-right: 0.25rem; + padding-top: 0; + padding-bottom: 0; +} + +<div> + <div + class="code-snippet-highlighted-oneline fs-mask emotion-0 emotion-1" + > + <button + 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> + <pre + class=" emotion-6 emotion-7" + > + <code + class="hljs" + > + <span + class="hljs-string" + > + foobar + </span> + </code> + </pre> + </div> +</div> +`; diff --git a/server/sonar-web/design-system/src/components/__tests__/__snapshots__/Highlighter-test.tsx.snap b/server/sonar-web/design-system/src/components/__tests__/__snapshots__/Highlighter-test.tsx.snap new file mode 100644 index 00000000000..45b4dd7b63b --- /dev/null +++ b/server/sonar-web/design-system/src/components/__tests__/__snapshots__/Highlighter-test.tsx.snap @@ -0,0 +1,463 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +.emotion-0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + overflow-x: auto; + padding: 1.5rem; +} + +.emotion-0 code { + color: rgb(51,53,60); + background: rgb(252,252,253); + font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 400; +} + +.emotion-0 code.hljs { + padding: unset; +} + +.emotion-0 .hljs-variable, +.emotion-0 .hljs-meta { + color: rgb(51,53,60); +} + +.emotion-0 .hljs-doctag, +.emotion-0 .hljs-title, +.emotion-0 .hljs-title.class_, +.emotion-0 .hljs-title.function_ { + color: rgb(34,84,192); +} + +.emotion-0 .hljs-comment { + color: rgb(109,111,119); + font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; + font-size: 0.875rem; + line-height: 1.25rem; + font-style: italic; +} + +.emotion-0 .hljs-tag, +.emotion-0 .hljs-type, +.emotion-0 .hljs-keyword { + color: rgb(152,29,150); + font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 700; +} + +.emotion-0 .hljs-literal, +.emotion-0 .hljs-number { + color: rgb(126,83,5); +} + +.emotion-0 .hljs-string { + color: rgb(32,105,31); +} + +.emotion-0 .hljs-meta .hljs-keyword { + color: rgb(47,103,48); +} + +.emotion-0.code-wrap { + white-space: pre-wrap; + word-break: break-all; +} + +.emotion-0 mark { + color: rgb(217,45,32); + background-color: rgb(197,205,223); + font-weight: 400; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.emotion-0.simple-one-line { + min-height: 1.25rem; + padding-left: 0.25rem; + padding-right: 0.25rem; + padding-top: 0; + padding-bottom: 0; +} + +<div> + <pre + class=" emotion-0 emotion-1" + > + <code + class="hljs" + > + <span + class="hljs-string" + > + foo\\nbar + </span> + </code> + </pre> +</div> +`; + +exports[`should display edit functions 1`] = ` +.emotion-0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + overflow-x: auto; + padding: 1.5rem; +} + +.emotion-0 code { + color: rgb(51,53,60); + background: rgb(252,252,253); + font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 400; +} + +.emotion-0 code.hljs { + padding: unset; +} + +.emotion-0 .hljs-variable, +.emotion-0 .hljs-meta { + color: rgb(51,53,60); +} + +.emotion-0 .hljs-doctag, +.emotion-0 .hljs-title, +.emotion-0 .hljs-title.class_, +.emotion-0 .hljs-title.function_ { + color: rgb(34,84,192); +} + +.emotion-0 .hljs-comment { + color: rgb(109,111,119); + font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; + font-size: 0.875rem; + line-height: 1.25rem; + font-style: italic; +} + +.emotion-0 .hljs-tag, +.emotion-0 .hljs-type, +.emotion-0 .hljs-keyword { + color: rgb(152,29,150); + font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 700; +} + +.emotion-0 .hljs-literal, +.emotion-0 .hljs-number { + color: rgb(126,83,5); +} + +.emotion-0 .hljs-string { + color: rgb(32,105,31); +} + +.emotion-0 .hljs-meta .hljs-keyword { + color: rgb(47,103,48); +} + +.emotion-0.code-wrap { + white-space: pre-wrap; + word-break: break-all; +} + +.emotion-0 mark { + color: rgb(217,45,32); + background-color: rgb(197,205,223); + font-weight: 400; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.emotion-0.simple-one-line { + min-height: 1.25rem; + padding-left: 0.25rem; + padding-right: 0.25rem; + padding-top: 0; + padding-bottom: 0; +} + +.emotion-3 { + box-sizing: border-box; + border: none; + outline: none; + -webkit-text-decoration: none; + text-decoration: none; + color: var(--color); + background-color: var(--background); + -webkit-transition: background-color 0.2s ease,outline 0.2s ease,color 0.2s ease; + transition: background-color 0.2s ease,outline 0.2s ease,color 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; + -webkit-box-pack: center; + -ms-flex-pack: center; + -webkit-justify-content: center; + justify-content: center; + cursor: pointer; + height: 2.25rem; + border-radius: 0.5rem; + padding-left: 0.625rem; + padding-right: 0.625rem; + --background: transparent; + --backgroundHover: rgb(232,235,255); + --color: rgb(75,86,187); + --colorHover: rgb(43,51,104); + --focus: rgba(93,108,208,0.2); +} + +.emotion-3:hover, +.emotion-3:focus, +.emotion-3:active { + color: var(--colorHover); + background-color: var(--backgroundHover); +} + +.emotion-3:focus, +.emotion-3:active { + outline: 4px solid var(--focus); +} + +.emotion-3:disabled, +.emotion-3:disabled:hover { + color: rgb(166,173,194); + background-color: var(--background); + cursor: not-allowed; +} + +<div> + <pre + class=" emotion-0 emotion-1" + > + <code + class="hljs sw-inline" + > + <span + class="hljs-string" + > + One + </span> + + <span + class="hljs-string" + > + line + </span> + + <span + class="hljs-string" + > + command + </span> + </code> + <button + aria-label="edit" + class="sw-ml-2 emotion-2 emotion-3 emotion-4" + type="button" + > + <svg + aria-hidden="true" + class="" + 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="M11.013 1.427a1.75 1.75 0 0 1 2.474 0l1.086 1.086a1.75 1.75 0 0 1 0 2.474l-8.61 8.61c-.21.21-.47.364-.756.445l-3.251.93a.75.75 0 0 1-.927-.928l.929-3.25c.081-.286.235-.547.445-.758l8.61-8.61Zm.176 4.823L9.75 4.81l-6.286 6.287a.253.253 0 0 0-.064.108l-.558 1.953 1.953-.558a.253.253 0 0 0 .108-.064Zm1.238-3.763a.25.25 0 0 0-.354 0L10.811 3.75l1.439 1.44 1.263-1.263a.25.25 0 0 0 0-.354Z" + /> + </svg> + </button> + </pre> +</div> +`; + +exports[`should handle multiple lines of code 1`] = ` +.emotion-0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + overflow-x: auto; + padding: 1.5rem; +} + +.emotion-0 code { + color: rgb(51,53,60); + background: rgb(252,252,253); + font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 400; +} + +.emotion-0 code.hljs { + padding: unset; +} + +.emotion-0 .hljs-variable, +.emotion-0 .hljs-meta { + color: rgb(51,53,60); +} + +.emotion-0 .hljs-doctag, +.emotion-0 .hljs-title, +.emotion-0 .hljs-title.class_, +.emotion-0 .hljs-title.function_ { + color: rgb(34,84,192); +} + +.emotion-0 .hljs-comment { + color: rgb(109,111,119); + font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; + font-size: 0.875rem; + line-height: 1.25rem; + font-style: italic; +} + +.emotion-0 .hljs-tag, +.emotion-0 .hljs-type, +.emotion-0 .hljs-keyword { + color: rgb(152,29,150); + font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 700; +} + +.emotion-0 .hljs-literal, +.emotion-0 .hljs-number { + color: rgb(126,83,5); +} + +.emotion-0 .hljs-string { + color: rgb(32,105,31); +} + +.emotion-0 .hljs-meta .hljs-keyword { + color: rgb(47,103,48); +} + +.emotion-0.code-wrap { + white-space: pre-wrap; + word-break: break-all; +} + +.emotion-0 mark { + color: rgb(217,45,32); + background-color: rgb(197,205,223); + font-weight: 400; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.emotion-0.simple-one-line { + min-height: 1.25rem; + padding-left: 0.25rem; + padding-right: 0.25rem; + padding-top: 0; + padding-bottom: 0; +} + +<div> + <pre + class=" emotion-0 emotion-1" + > + <code + class="hljs" + > + <span + class="hljs-attr" + > + foo: + </span> + + <span + class="hljs-string" + > + bar + </span> + + + <span + class="hljs-attr" + > + pleh: + </span> + + <span + class="hljs-string" + > + help + </span> + + + <span + class="hljs-attr" + > + stuff: + </span> + + + <span + class="hljs-attr" + > + foo: + </span> + + <span + class="hljs-string" + > + bar + </span> + + + <span + class="hljs-attr" + > + bar: + </span> + + <span + class="hljs-string" + > + foo + </span> + </code> + </pre> +</div> +`; diff --git a/server/sonar-web/design-system/src/components/icons/PencilIcon.tsx b/server/sonar-web/design-system/src/components/icons/PencilIcon.tsx new file mode 100644 index 00000000000..5fec48bae65 --- /dev/null +++ b/server/sonar-web/design-system/src/components/icons/PencilIcon.tsx @@ -0,0 +1,23 @@ +/* + * 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 { PencilIcon as PencilOcticonIcon } from '@primer/octicons-react'; +import { OcticonHoc } from './Icon'; + +export const PencilIcon = OcticonHoc(PencilOcticonIcon); diff --git a/server/sonar-web/design-system/src/components/icons/index.ts b/server/sonar-web/design-system/src/components/icons/index.ts index ce7c413ff63..6f4a7a4c893 100644 --- a/server/sonar-web/design-system/src/components/icons/index.ts +++ b/server/sonar-web/design-system/src/components/icons/index.ts @@ -49,6 +49,7 @@ export { OpenCloseIndicator } from './OpenCloseIndicator'; export { OpenNewTabIcon } from './OpenNewTabIcon'; export { OverviewQGNotComputedIcon } from './OverviewQGNotComputedIcon'; export { OverviewQGPassedIcon } from './OverviewQGPassedIcon'; +export { PencilIcon } from './PencilIcon'; export { ProjectIcon } from './ProjectIcon'; export { PullRequestIcon } from './PullRequestIcon'; export { RefreshIcon } from './RefreshIcon'; diff --git a/server/sonar-web/design-system/src/components/index.ts b/server/sonar-web/design-system/src/components/index.ts index 7fefefbfc7f..51a75a9969c 100644 --- a/server/sonar-web/design-system/src/components/index.ts +++ b/server/sonar-web/design-system/src/components/index.ts @@ -23,6 +23,7 @@ export * from './Avatar'; export { Badge } from './Badge'; export { BarChart } from './BarChart'; export * from './Card'; +export * from './CodeSnippet'; export * from './CoverageIndicator'; export * from './DatePicker'; export * from './DateRangePicker'; diff --git a/server/sonar-web/design-system/src/theme/light.ts b/server/sonar-web/design-system/src/theme/light.ts index 7a3756efbde..4854e46adb7 100644 --- a/server/sonar-web/design-system/src/theme/light.ts +++ b/server/sonar-web/design-system/src/theme/light.ts @@ -43,6 +43,17 @@ const danger = { darker: COLORS.red[800], }; +const codeSnippetLight = { + annotations: [34, 84, 192], + body: [51, 53, 60], + constants: [126, 83, 5], + comments: [109, 111, 119], + keyword: [152, 29, 150], + string: [32, 105, 31], + 'keyword-light': [28, 28, 163], // Not used currently in code snippet + 'preprocessing-directive': [47, 103, 48], +}; + export const lightTheme = { id: 'light-theme', highlightTheme: 'atom-one-light.css', @@ -170,6 +181,15 @@ export const lightTheme = { codeSnippetBackground: COLORS.blueGrey[25], codeSnippetBorder: COLORS.blueGrey[100], codeSnippetHighlight: secondary.default, + codeSnippetBody: codeSnippetLight.body, + codeSnippetAnnotations: codeSnippetLight.annotations, + codeSnippetComments: codeSnippetLight.comments, + codeSnippetConstants: codeSnippetLight.constants, + codeSnippetKeyword: codeSnippetLight.keyword, + codeSnippetString: codeSnippetLight.string, + codeSnippetKeywordLight: codeSnippetLight['keyword-light'], + codeSnippetPreprocessingDirective: codeSnippetLight['preprocessing-directive'], + codeSnippetInline: COLORS.blueGrey[500], // code viewer codeLineIssueIndicator: COLORS.blueGrey[400], // Should be blueGrey[300], to be changed once code viewer is reworked diff --git a/server/sonar-web/yarn.lock b/server/sonar-web/yarn.lock index 1847ec4d86a..90e4d51539d 100644 --- a/server/sonar-web/yarn.lock +++ b/server/sonar-web/yarn.lock @@ -6146,6 +6146,7 @@ __metadata: eslint-plugin-import: 2.27.5 eslint-plugin-local-rules: 1.3.2 eslint-plugin-typescript-sort-keys: 2.3.0 + highlight.js: 11.7.0 history: 5.3.0 jest: 29.5.0 postcss: 8.4.21 @@ -7839,6 +7840,13 @@ __metadata: languageName: node linkType: hard +"highlight.js@npm:11.7.0": + version: 11.7.0 + resolution: "highlight.js@npm:11.7.0" + checksum: 19e3fb8b56f4b361b057a8523b989dfeb6479bbd1e29cec3fac6fa5c78d09927d5fa61b7dba6631fdb57cfdca9b3084aa4da49405ceaf4a67f67beae2ed5b77d + languageName: node + linkType: hard + "history@npm:5.3.0": version: 5.3.0 resolution: "history@npm:5.3.0" |