]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19638 Add syntax highlighting to code snippets in rule details
authorDavid Cho-Lerat <david.cho-lerat@sonarsource.com>
Thu, 22 Jun 2023 15:04:33 +0000 (17:04 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 23 Jun 2023 20:03:17 +0000 (20:03 +0000)
24 files changed:
server/sonar-web/design-system/package.json
server/sonar-web/design-system/src/@types/highlightjs-apex.d.ts [new file with mode: 0644]
server/sonar-web/design-system/src/@types/highlightjs-sap-abap.d.ts [new file with mode: 0644]
server/sonar-web/design-system/src/components/CodeSnippet.tsx [deleted file]
server/sonar-web/design-system/src/components/CodeSyntaxHighlighter.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/Highlighter.tsx [deleted file]
server/sonar-web/design-system/src/components/__tests__/CodeSnippet-test.tsx [deleted file]
server/sonar-web/design-system/src/components/__tests__/CodeSyntaxHighlighter-test.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/__tests__/Highlighter-test.tsx [deleted file]
server/sonar-web/design-system/src/components/__tests__/__snapshots__/CodeSnippet-test.tsx.snap [deleted file]
server/sonar-web/design-system/src/components/__tests__/__snapshots__/Highlighter-test.tsx.snap [deleted file]
server/sonar-web/design-system/src/components/index.ts
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx
server/sonar-web/src/main/js/components/common/__tests__/AnalysisWarningsModal-test.tsx
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/AnalysisWarningsModal-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/components/rules/IssueTabViewer.tsx
server/sonar-web/src/main/js/components/rules/MoreInfoRuleDescription.tsx
server/sonar-web/src/main/js/components/rules/RuleDescription.tsx
server/sonar-web/src/main/js/components/rules/RuleTabViewer.tsx
server/sonar-web/src/main/js/helpers/mocks/tasks.ts
server/sonar-web/yarn.lock

index 2ca100176697e86b7eb7af36e5f807c69c2fc13d..3d8cf39ac8716ca78b7cfdd33f56e54d1a3c3510 100644 (file)
@@ -35,6 +35,9 @@
     "eslint-plugin-local-rules": "1.3.2",
     "eslint-plugin-typescript-sort-keys": "2.3.0",
     "highlight.js": "11.7.0",
+    "highlightjs-apex": "1.2.0",
+    "highlightjs-cobol": "0.3.3",
+    "highlightjs-sap-abap": "0.2.0",
     "history": "5.3.0",
     "jest": "29.5.0",
     "postcss": "8.4.21",
diff --git a/server/sonar-web/design-system/src/@types/highlightjs-apex.d.ts b/server/sonar-web/design-system/src/@types/highlightjs-apex.d.ts
new file mode 100644 (file)
index 0000000..efcfd75
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+declare module 'highlightjs-apex' {
+  import { LanguageFn } from 'highlight.js';
+
+  const defineLanguage: LanguageFn;
+  // eslint-disable-next-line import/no-default-export
+  export default defineLanguage;
+}
diff --git a/server/sonar-web/design-system/src/@types/highlightjs-sap-abap.d.ts b/server/sonar-web/design-system/src/@types/highlightjs-sap-abap.d.ts
new file mode 100644 (file)
index 0000000..d133b6e
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+declare module 'highlightjs-sap-abap' {
+  import { LanguageFn } from 'highlight.js';
+
+  const defineLanguage: LanguageFn;
+  // eslint-disable-next-line import/no-default-export
+  export default defineLanguage;
+}
diff --git a/server/sonar-web/design-system/src/components/CodeSnippet.tsx b/server/sonar-web/design-system/src/components/CodeSnippet.tsx
deleted file mode 100644 (file)
index e1fed4f..0000000
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * 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/CodeSyntaxHighlighter.tsx b/server/sonar-web/design-system/src/components/CodeSyntaxHighlighter.tsx
new file mode 100644 (file)
index 0000000..487b222
--- /dev/null
@@ -0,0 +1,153 @@
+/*
+ * 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 hljs from 'highlight.js';
+import apex from 'highlightjs-apex';
+import cobol from 'highlightjs-cobol';
+import abap from 'highlightjs-sap-abap';
+import tw from 'twin.macro';
+import { themeColor, themeContrast } from '../helpers/theme';
+
+hljs.registerLanguage('abap', abap);
+hljs.registerLanguage('apex', apex);
+hljs.registerLanguage('cobol', cobol);
+
+hljs.registerAliases('azureresourcemanager', { languageName: 'json' });
+hljs.registerAliases('flex', { languageName: 'actionscript' });
+hljs.registerAliases('objc', { languageName: 'objectivec' });
+hljs.registerAliases('plsql', { languageName: 'pgsql' });
+hljs.registerAliases('secrets', { languageName: 'markdown' });
+hljs.registerAliases('web', { languageName: 'xml' });
+hljs.registerAliases(['cloudformation', 'kubernetes'], { languageName: 'yaml' });
+
+interface Props {
+  className?: string;
+  htmlAsString: string;
+  language?: string;
+}
+
+const CODE_REGEXP = '<(code|pre)\\b([^>]*?)>(.+?)<\\/\\1>';
+const GLOBAL_REGEXP = new RegExp(CODE_REGEXP, 'gs');
+const SINGLE_REGEXP = new RegExp(CODE_REGEXP, 's');
+
+const htmlDecode = (escapedCode: string) => {
+  const doc = new DOMParser().parseFromString(escapedCode, 'text/html');
+
+  return doc.documentElement.textContent ?? '';
+};
+
+export function CodeSyntaxHighlighter({ className, htmlAsString, language }: Props) {
+  let highlightedHtmlAsString = htmlAsString;
+
+  htmlAsString.match(GLOBAL_REGEXP)?.forEach((codeBlock) => {
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    const [, tag, attributes, code] = SINGLE_REGEXP.exec(codeBlock)!;
+
+    const unescapedCode = htmlDecode(code);
+
+    let highlightedCode;
+
+    try {
+      highlightedCode = hljs.highlight(unescapedCode, {
+        ignoreIllegals: true,
+        language: language ?? 'plaintext',
+      });
+    } catch {
+      highlightedCode = hljs.highlight(unescapedCode, {
+        ignoreIllegals: true,
+        language: 'plaintext',
+      });
+    }
+
+    highlightedHtmlAsString = highlightedHtmlAsString.replace(
+      codeBlock,
+      `<${tag}${attributes}>${highlightedCode.value}</${tag}>`
+    );
+  });
+
+  return (
+    <StyledSpan
+      className={`hljs ${className ?? ''}`}
+      // Safe: value is escaped by highlight.js
+      // eslint-disable-next-line react/no-danger
+      dangerouslySetInnerHTML={{ __html: highlightedHtmlAsString }}
+    />
+  );
+}
+
+const StyledSpan = styled.span`
+  code {
+    ${tw`sw-code`};
+
+    background: ${themeColor('codeSnippetBackground')};
+    color: ${themeColor('codeSnippetBody')};
+
+    &.hljs {
+      padding: unset;
+    }
+  }
+
+  .hljs-meta,
+  .hljs-variable {
+    color: ${themeColor('codeSnippetBody')};
+  }
+
+  .hljs-doctag,
+  .hljs-title,
+  .hljs-title.class_,
+  .hljs-title.function_ {
+    color: ${themeColor('codeSnippetAnnotations')};
+  }
+
+  .hljs-comment {
+    ${tw`sw-code-comment`}
+
+    color: ${themeColor('codeSnippetComments')};
+  }
+
+  .hljs-keyword,
+  .hljs-tag,
+  .hljs-type {
+    color: ${themeColor('codeSnippetKeyword')};
+  }
+
+  .hljs-literal,
+  .hljs-number {
+    color: ${themeColor('codeSnippetConstants')};
+  }
+
+  .hljs-string {
+    color: ${themeColor('codeSnippetString')};
+  }
+
+  .hljs-meta .hljs-keyword {
+    color: ${themeColor('codeSnippetPreprocessingDirective')};
+  }
+
+  mark {
+    ${tw`sw-font-regular`}
+    ${tw`sw-p-1`}
+    ${tw`sw-rounded-1`}
+
+    background-color: ${themeColor('codeSnippetHighlight')};
+    color: ${themeContrast('codeSnippetHighlight')};
+  }
+`;
diff --git a/server/sonar-web/design-system/src/components/Highlighter.tsx b/server/sonar-web/design-system/src/components/Highlighter.tsx
deleted file mode 100644 (file)
index 7369031..0000000
+++ /dev/null
@@ -1,182 +0,0 @@
-/*
- * 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(/&lt;mark&gt;/g, '<mark>')
-      .replace(/&lt;\/mark&gt;/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
deleted file mode 100644 (file)
index 87bca8a..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * 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__/CodeSyntaxHighlighter-test.tsx b/server/sonar-web/design-system/src/components/__tests__/CodeSyntaxHighlighter-test.tsx
new file mode 100644 (file)
index 0000000..a55e079
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * 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 { render } from '../../helpers/testUtils';
+import { CodeSyntaxHighlighter } from '../CodeSyntaxHighlighter';
+
+it('renders correctly with no code', () => {
+  const { container } = render(
+    <CodeSyntaxHighlighter
+      htmlAsString={`
+        <p>Hello there!</p>
+
+        <p>There's no code here.</p>
+      `}
+    />
+  );
+
+  // eslint-disable-next-line testing-library/no-node-access
+  expect(container.getElementsByClassName('hljs-string').length).toBe(0);
+});
+
+it('renders correctly with code', () => {
+  const { container } = render(
+    <CodeSyntaxHighlighter
+      htmlAsString={`
+        <p>Hello there!</p>
+
+        <p>There's some <code>"code"</code> here.</p>
+     `}
+      language="typescript"
+    />
+  );
+
+  // eslint-disable-next-line testing-library/no-node-access
+  expect(container.getElementsByClassName('hljs-string').length).toBe(1);
+});
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
deleted file mode 100644 (file)
index c02ca7f..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * 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
deleted file mode 100644 (file)
index ec00317..0000000
+++ /dev/null
@@ -1,473 +0,0 @@
-// 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.125rem;
-  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.125rem;
-  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.125rem;
-  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
-      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>
-    <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.125rem;
-  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.125rem;
-  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.125rem;
-  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
-      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>
-    <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
deleted file mode 100644 (file)
index d7fed05..0000000
+++ /dev/null
@@ -1,463 +0,0 @@
-// 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.125rem;
-  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.125rem;
-  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.125rem;
-  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.125rem;
-  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.125rem;
-  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.125rem;
-  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.125rem;
-  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.125rem;
-  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.125rem;
-  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>
-`;
index 2724f1199d8a360011c59243bbc59bb07bddae5f..dbe05f21f679f09c0e9bd4f64c71539b14750e76 100644 (file)
@@ -26,7 +26,7 @@ export { Breadcrumbs } from './Breadcrumbs';
 export * from './BubbleChart';
 export * from './Card';
 export * from './Checkbox';
-export * from './CodeSnippet';
+export * from './CodeSyntaxHighlighter';
 export * from './ColorsLegend';
 export * from './CoverageIndicator';
 export * from './DatePicker';
index 27c79da9e56d0ac4039fa6e325bb65e95c5f0797..3a66d28122c274286255b01386266c2a64758551 100644 (file)
@@ -17,6 +17,8 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+
+import { CodeSyntaxHighlighter } from 'design-system';
 import * as React from 'react';
 import { updateRule } from '../../../api/rules';
 import FormattingTips from '../../../components/common/FormattingTips';
@@ -46,8 +48,8 @@ export default class RuleDetailsDescription extends React.PureComponent<Props, S
   state: State = {
     description: '',
     descriptionForm: false,
-    submitting: false,
     removeDescriptionModal: false,
+    submitting: false,
   };
 
   componentDidMount() {
@@ -89,6 +91,7 @@ export default class RuleDetailsDescription extends React.PureComponent<Props, S
     }).then(
       (ruleDetails) => {
         this.props.onChange(ruleDetails);
+
         if (this.mounted) {
           this.setState({ submitting: false, descriptionForm: false });
         }
@@ -104,7 +107,7 @@ export default class RuleDetailsDescription extends React.PureComponent<Props, S
   handleExtendDescriptionClick = () => {
     this.setState({
       // set description` to the current `mdNote` each time the form is open
-      description: this.props.ruleDetails.mdNote || '',
+      description: this.props.ruleDetails.mdNote ?? '',
       descriptionForm: true,
     });
   };
@@ -112,14 +115,13 @@ export default class RuleDetailsDescription extends React.PureComponent<Props, S
   renderExtendedDescription = () => (
     <div id="coding-rules-detail-description-extra">
       {this.props.ruleDetails.htmlNote !== undefined && (
-        <div
-          className="rule-desc spacer-bottom markdown"
-          // eslint-disable-next-line react/no-danger
-          dangerouslySetInnerHTML={{
-            __html: sanitizeUserInput(this.props.ruleDetails.htmlNote),
-          }}
+        <CodeSyntaxHighlighter
+          className="rule-desc markdown sw-mb-2"
+          htmlAsString={sanitizeUserInput(this.props.ruleDetails.htmlNote)}
+          language={this.props.ruleDetails.lang}
         />
       )}
+
       {this.props.canWrite && (
         <Button
           id="coding-rules-detail-extend-description"
@@ -147,6 +149,7 @@ export default class RuleDetailsDescription extends React.PureComponent<Props, S
               />
             </td>
           </tr>
+
           <tr>
             <td>
               <Button
@@ -156,6 +159,7 @@ export default class RuleDetailsDescription extends React.PureComponent<Props, S
               >
                 {translate('save')}
               </Button>
+
               {this.props.ruleDetails.mdNote !== undefined && (
                 <>
                   <Button
@@ -174,6 +178,7 @@ export default class RuleDetailsDescription extends React.PureComponent<Props, S
                   )}
                 </>
               )}
+
               <ResetButtonLink
                 className="spacer-left"
                 disabled={this.state.submitting}
@@ -184,6 +189,7 @@ export default class RuleDetailsDescription extends React.PureComponent<Props, S
               </ResetButtonLink>
               {this.state.submitting && <i className="spinner spacer-left" />}
             </td>
+
             <td className="text-right">
               <FormattingTips />
             </td>
@@ -216,23 +222,24 @@ export default class RuleDetailsDescription extends React.PureComponent<Props, S
     return (
       <div className="js-rule-description">
         {defaultSection && (
-          <section
+          <CodeSyntaxHighlighter
             className="coding-rules-detail-description markdown"
             key={defaultSection.key}
-            /* eslint-disable-next-line react/no-danger */
-            dangerouslySetInnerHTML={{ __html: sanitizeString(defaultSection.content) }}
+            htmlAsString={sanitizeString(defaultSection.content)}
+            language={ruleDetails.lang}
           />
         )}
 
         {hasDescriptionSection && !defaultSection && (
           <>
             {introductionSection && (
-              <div
+              <CodeSyntaxHighlighter
                 className="rule-desc"
-                // eslint-disable-next-line react/no-danger
-                dangerouslySetInnerHTML={{ __html: sanitizeString(introductionSection) }}
+                htmlAsString={sanitizeString(introductionSection)}
+                language={ruleDetails.lang}
               />
             )}
+
             <RuleTabViewer ruleDetails={ruleDetails} />
           </>
         )}
index 0a9e334bd08a2aad17e654224b3c74adf9bc551c..e62c665ea426ee3f21a6d30f0fc3d97235180692 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+
 import * as React from 'react';
 import { getRuleDetails } from '../../../api/rules';
 import { getSecurityHotspotDetails } from '../../../api/security-hotspots';
@@ -36,19 +37,20 @@ import HotspotViewerRenderer from './HotspotViewerRenderer';
 interface Props {
   component: Component;
   hotspotKey: string;
-  onSwitchStatusFilter: (option: HotspotStatusFilter) => void;
   hotspotsReviewedMeasure?: string;
-  onUpdateHotspot: (hotspotKey: string) => Promise<void>;
   onLocationClick: (index: number) => void;
+  onSwitchStatusFilter: (option: HotspotStatusFilter) => void;
+  onUpdateHotspot: (hotspotKey: string) => Promise<void>;
   selectedHotspotLocation?: number;
   standards?: Standards;
 }
 
 interface State {
   hotspot?: Hotspot;
-  ruleDescriptionSections?: RuleDescriptionSection[];
   lastStatusChangedTo?: HotspotStatusOption;
   loading: boolean;
+  ruleDescriptionSections?: RuleDescriptionSection[];
+  ruleLanguage?: string;
   showStatusUpdateSuccessModal: boolean;
 }
 
@@ -78,6 +80,7 @@ export default class HotspotViewer extends React.PureComponent<Props, State> {
 
   fetchHotspot = async () => {
     this.setState({ loading: true });
+
     try {
       const hotspot = await getSecurityHotspotDetails(this.props.hotspotKey);
       const ruleDetails = await getRuleDetails({ key: hotspot.rule.key }).then((r) => r.rule);
@@ -86,6 +89,7 @@ export default class HotspotViewer extends React.PureComponent<Props, State> {
         this.setState({
           hotspot,
           loading: false,
+          ruleLanguage: ruleDetails.lang,
           ruleDescriptionSections: ruleDetails.descriptionSections,
         });
       }
@@ -112,6 +116,7 @@ export default class HotspotViewer extends React.PureComponent<Props, State> {
 
   handleSwitchFilterToStatusOfUpdatedHotspot = () => {
     const { lastStatusChangedTo } = this.state;
+
     if (lastStatusChangedTo) {
       this.props.onSwitchStatusFilter(getStatusFilterFromStatusOption(lastStatusChangedTo));
     }
@@ -122,10 +127,12 @@ export default class HotspotViewer extends React.PureComponent<Props, State> {
   };
 
   render() {
-    const { component, selectedHotspotLocation, standards, hotspotsReviewedMeasure } = this.props;
+    const { component, hotspotsReviewedMeasure, selectedHotspotLocation, standards } = this.props;
+
     const {
       hotspot,
       ruleDescriptionSections,
+      ruleLanguage,
       loading,
       showStatusUpdateSuccessModal,
       lastStatusChangedTo,
@@ -133,19 +140,20 @@ export default class HotspotViewer extends React.PureComponent<Props, State> {
 
     return (
       <HotspotViewerRenderer
+        component={component}
+        hotspot={hotspot}
         hotspotsReviewedMeasure={hotspotsReviewedMeasure}
         lastStatusChangedTo={lastStatusChangedTo}
+        loading={loading}
         onCloseStatusUpdateSuccessModal={this.handleCloseStatusUpdateSuccessModal}
+        onLocationClick={this.props.onLocationClick}
         onSwitchFilterToStatusOfUpdatedHotspot={this.handleSwitchFilterToStatusOfUpdatedHotspot}
-        showStatusUpdateSuccessModal={showStatusUpdateSuccessModal}
-        standards={standards}
-        component={component}
-        hotspot={hotspot}
-        ruleDescriptionSections={ruleDescriptionSections}
-        loading={loading}
         onUpdateHotspot={this.handleHotspotUpdate}
-        onLocationClick={this.props.onLocationClick}
+        ruleDescriptionSections={ruleDescriptionSections}
+        ruleLanguage={ruleLanguage}
         selectedHotspotLocation={selectedHotspotLocation}
+        showStatusUpdateSuccessModal={showStatusUpdateSuccessModal}
+        standards={standards}
       />
     );
   }
index 6e4ae9e6798d7090f53594a422a4d0256a94fa70..8aedbfe45d0355851fc2e45c748959c3f67e35f4 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+
 import * as React from 'react';
 import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext';
 import DeferredSpinner from '../../../components/ui/DeferredSpinner';
@@ -36,17 +37,17 @@ export interface HotspotViewerRendererProps {
   component: Component;
   currentUser: CurrentUser;
   hotspot?: Hotspot;
-  ruleDescriptionSections?: RuleDescriptionSection[];
   hotspotsReviewedMeasure?: string;
-  onSwitchFilterToStatusOfUpdatedHotspot: () => void;
   lastStatusChangedTo?: HotspotStatusOption;
-  onCloseStatusUpdateSuccessModal: () => void;
-  showStatusUpdateSuccessModal: boolean;
-
   loading: boolean;
-  onUpdateHotspot: (statusUpdate?: boolean, statusOption?: HotspotStatusOption) => Promise<void>;
+  onCloseStatusUpdateSuccessModal: () => void;
   onLocationClick: (index: number) => void;
+  onSwitchFilterToStatusOfUpdatedHotspot: () => void;
+  onUpdateHotspot: (statusUpdate?: boolean, statusOption?: HotspotStatusOption) => Promise<void>;
+  ruleDescriptionSections?: RuleDescriptionSection[];
+  ruleLanguage?: string;
   selectedHotspotLocation?: number;
+  showStatusUpdateSuccessModal: boolean;
   standards?: Standards;
 }
 
@@ -55,12 +56,13 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
     component,
     currentUser,
     hotspot,
+    hotspotsReviewedMeasure,
+    lastStatusChangedTo,
     loading,
-    selectedHotspotLocation,
     ruleDescriptionSections,
+    ruleLanguage,
+    selectedHotspotLocation,
     showStatusUpdateSuccessModal,
-    hotspotsReviewedMeasure,
-    lastStatusChangedTo,
     standards,
   } = props;
 
@@ -89,6 +91,7 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
                 onCommentUpdate={props.onUpdateHotspot}
               />
             }
+            branchLike={branchLike}
             codeTabContent={
               <HotspotSnippetContainer
                 branchLike={branchLike}
@@ -99,11 +102,11 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
               />
             }
             component={component}
-            standards={standards}
-            onUpdateHotspot={props.onUpdateHotspot}
-            branchLike={branchLike}
             hotspot={hotspot}
+            onUpdateHotspot={props.onUpdateHotspot}
             ruleDescriptionSections={ruleDescriptionSections}
+            ruleLanguage={ruleLanguage}
+            standards={standards}
           />
         </div>
       )}
index 23e62039ee820b7de2a3bd0ab3c9226c2d54da95..2401739e699fb8856903f12d5234ccde10b8d773 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+
 import { ToggleButton, getTabId, getTabPanelId } from 'design-system';
 import { groupBy, omit } from 'lodash';
 import * as React from 'react';
@@ -34,18 +35,20 @@ import { HotspotHeader } from './HotspotHeader';
 
 interface Props {
   activityTabContent: React.ReactNode;
+  branchLike?: BranchLike;
   codeTabContent: React.ReactNode;
-  hotspot: Hotspot;
-  ruleDescriptionSections?: RuleDescriptionSection[];
   component: Component;
-  branchLike?: BranchLike;
+  hotspot: Hotspot;
   onUpdateHotspot: (statusUpdate?: boolean, statusOption?: HotspotStatusOption) => Promise<void>;
+  ruleDescriptionSections?: RuleDescriptionSection[];
+  ruleLanguage?: string;
   standards?: Standards;
 }
+
 interface Tab {
-  value: TabKeys;
-  label: string;
   counter?: number;
+  label: string;
+  value: TabKeys;
 }
 
 export enum TabKeys {
@@ -61,13 +64,14 @@ const STICKY_HEADER_COMPRESS_THRESHOLD = 200;
 
 export default function HotspotViewerTabs(props: Props) {
   const {
-    ruleDescriptionSections,
-    codeTabContent,
     activityTabContent,
-    hotspot,
+    branchLike,
+    codeTabContent,
     component,
+    hotspot,
+    ruleDescriptionSections,
+    ruleLanguage,
     standards,
-    branchLike,
   } = props;
 
   const { isScrolled, isCompressed, resetScrollDownCompress } = useScrollDownCompress(
@@ -119,6 +123,7 @@ export default function HotspotViewerTabs(props: Props) {
     if (isInput(event) || isShortcut(event)) {
       return true;
     }
+
     if (event.key === KeyboardKeys.LeftArrow) {
       event.preventDefault();
       selectNeighboringTab(-1);
@@ -143,6 +148,7 @@ export default function HotspotViewerTabs(props: Props) {
 
   const handleSelectTabs = (tabKey: TabKeys) => {
     const currentTab = tabs.find((tab) => tab.value === tabKey);
+
     if (currentTab) {
       setCurrentTab(currentTab);
     }
@@ -152,10 +158,12 @@ export default function HotspotViewerTabs(props: Props) {
     document.addEventListener('keydown', handleKeyboardNavigation);
 
     return () => document.removeEventListener('keydown', handleKeyboardNavigation);
+    // eslint-disable-next-line react-hooks/exhaustive-deps
   }, []);
 
   React.useEffect(() => {
     setCurrentTab(tabs[0]);
+    // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [hotspot.key]);
 
   React.useEffect(() => {
@@ -163,9 +171,11 @@ export default function HotspotViewerTabs(props: Props) {
       window.scrollTo({ top: 0 });
     }
     resetScrollDownCompress();
+    // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [currentTab]);
 
   const descriptionSectionsByKey = groupBy(ruleDescriptionSections, (section) => section.key);
+
   const rootCauseDescriptionSections =
     descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] ||
     descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE];
@@ -173,13 +183,13 @@ export default function HotspotViewerTabs(props: Props) {
   return (
     <>
       <HotspotHeader
-        hotspot={hotspot}
-        component={component}
-        standards={standards}
-        onUpdateHotspot={props.onUpdateHotspot}
         branchLike={branchLike}
-        isScrolled={isScrolled}
+        component={component}
+        hotspot={hotspot}
         isCompressed={isCompressed}
+        isScrolled={isScrolled}
+        onUpdateHotspot={props.onUpdateHotspot}
+        standards={standards}
         tabs={
           <ToggleButton
             role="tablist"
@@ -198,12 +208,13 @@ export default function HotspotViewerTabs(props: Props) {
         {currentTab.value === TabKeys.Code && codeTabContent}
 
         {currentTab.value === TabKeys.RiskDescription && rootCauseDescriptionSections && (
-          <RuleDescription sections={rootCauseDescriptionSections} />
+          <RuleDescription language={ruleLanguage} sections={rootCauseDescriptionSections} />
         )}
 
         {currentTab.value === TabKeys.VulnerabilityDescription &&
           descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM] && (
             <RuleDescription
+              language={ruleLanguage}
               sections={descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM]}
             />
           )}
@@ -211,6 +222,7 @@ export default function HotspotViewerTabs(props: Props) {
         {currentTab.value === TabKeys.FixRecommendation &&
           descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX] && (
             <RuleDescription
+              language={ruleLanguage}
               sections={descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX]}
             />
           )}
index ca8be50d77f7881943f2560cf809a2819e96050b..28bb2f1ddf8ef19d8712fc69f3ef5996a2f1fb9b 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { shallow } from 'enzyme';
+
+import { screen, waitFor } from '@testing-library/react';
 import * as React from 'react';
 import { getTask } from '../../../api/ce';
 import { mockTaskWarning } from '../../../helpers/mocks/tasks';
 import { mockCurrentUser } from '../../../helpers/testMocks';
-import { waitAndUpdate } from '../../../helpers/testUtils';
+import { renderComponent } from '../../../helpers/testReactTestingUtils';
 import { AnalysisWarningsModal } from '../AnalysisWarningsModal';
 
 jest.mock('../../../api/ce', () => ({
@@ -34,48 +35,50 @@ jest.mock('../../../api/ce', () => ({
 
 beforeEach(jest.clearAllMocks);
 
-it('should render correctly', () => {
-  expect(shallowRender()).toMatchSnapshot('default');
-  expect(shallowRender({ warnings: [mockTaskWarning({ dismissable: true })] })).toMatchSnapshot(
-    'with dismissable warnings'
-  );
-  expect(
-    shallowRender({
+describe('should render correctly', () => {
+  it('should not show dismiss buttons for non-dismissable warnings', () => {
+    renderAnalysisWarningsModal();
+
+    expect(screen.getByText('warning 1')).toBeInTheDocument();
+    expect(screen.getByText('warning 2')).toBeInTheDocument();
+    expect(screen.queryByRole('button', { name: 'dismiss_permanently' })).not.toBeInTheDocument();
+  });
+
+  it('should show a dismiss button for dismissable warnings', () => {
+    renderAnalysisWarningsModal({ warnings: [mockTaskWarning({ dismissable: true })] });
+
+    expect(screen.getByRole('button', { name: 'dismiss_permanently' })).toBeInTheDocument();
+  });
+
+  it('should not show dismiss buttons if not logged in', () => {
+    renderAnalysisWarningsModal({
       currentUser: mockCurrentUser({ isLoggedIn: false }),
       warnings: [mockTaskWarning({ dismissable: true })],
-    })
-  ).toMatchSnapshot('do not show dismissable links for anonymous');
+    });
+
+    expect(screen.queryByRole('button', { name: 'dismiss_permanently' })).not.toBeInTheDocument();
+  });
 });
 
 it('should not fetch task warnings if it does not have to', () => {
-  shallowRender();
+  renderAnalysisWarningsModal();
+
   expect(getTask).not.toHaveBeenCalled();
 });
 
 it('should fetch task warnings if it has to', async () => {
-  const wrapper = shallowRender({ taskId: 'abcd1234', warnings: undefined });
-  await waitAndUpdate(wrapper);
-  expect(wrapper).toMatchSnapshot();
-  expect(getTask).toHaveBeenCalledWith('abcd1234', ['warnings']);
-});
+  renderAnalysisWarningsModal({ taskId: 'abcd1234', warnings: undefined });
 
-it('should correctly handle updates', async () => {
-  const wrapper = shallowRender();
-
-  await waitAndUpdate(wrapper);
-  expect(getTask).not.toHaveBeenCalled();
-
-  wrapper.setProps({ taskId: '1', warnings: undefined });
-  await waitAndUpdate(wrapper);
-  expect(getTask).toHaveBeenCalled();
+  expect(screen.queryByText('message foo')).not.toBeInTheDocument();
+  expect(getTask).toHaveBeenCalledWith('abcd1234', ['warnings']);
 
-  (getTask as jest.Mock).mockClear();
-  wrapper.setProps({ taskId: undefined, warnings: [mockTaskWarning()] });
-  expect(getTask).not.toHaveBeenCalled();
+  await waitFor(() => {
+    expect(screen.getByText('message foo')).toBeInTheDocument();
+  });
 });
 
-function shallowRender(props: Partial<AnalysisWarningsModal['props']> = {}) {
-  return shallow<AnalysisWarningsModal>(
+function renderAnalysisWarningsModal(props: Partial<AnalysisWarningsModal['props']> = {}) {
+  return renderComponent(
     <AnalysisWarningsModal
       currentUser={mockCurrentUser({ isLoggedIn: true })}
       onClose={jest.fn()}
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/AnalysisWarningsModal-test.tsx.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/AnalysisWarningsModal-test.tsx.snap
deleted file mode 100644 (file)
index d739337..0000000
+++ /dev/null
@@ -1,216 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should fetch task warnings if it has to 1`] = `
-<Gl
-  body={
-    <DeferredSpinner
-      loading={false}
-    >
-      <React.Fragment>
-        <div
-          className="sw-flex sw-items-center sw-mt-2"
-        >
-          <FlagMessage
-            variant="warning"
-          >
-            <HtmlFormatter>
-              <span
-                dangerouslySetInnerHTML={
-                  {
-                    "__html": "message foo",
-                  }
-                }
-              />
-            </HtmlFormatter>
-          </FlagMessage>
-        </div>
-        <div />
-      </React.Fragment>
-      <React.Fragment>
-        <div
-          className="sw-flex sw-items-center sw-mt-2"
-        >
-          <FlagMessage
-            variant="warning"
-          >
-            <HtmlFormatter>
-              <span
-                dangerouslySetInnerHTML={
-                  {
-                    "__html": "message-bar",
-                  }
-                }
-              />
-            </HtmlFormatter>
-          </FlagMessage>
-        </div>
-        <div />
-      </React.Fragment>
-      <React.Fragment>
-        <div
-          className="sw-flex sw-items-center sw-mt-2"
-        >
-          <FlagMessage
-            variant="warning"
-          >
-            <HtmlFormatter>
-              <span
-                dangerouslySetInnerHTML={
-                  {
-                    "__html": "multiline message<br>secondline<br>  third line",
-                  }
-                }
-              />
-            </HtmlFormatter>
-          </FlagMessage>
-        </div>
-        <div />
-      </React.Fragment>
-    </DeferredSpinner>
-  }
-  headerTitle="warnings"
-  onClose={[MockFunction]}
-  primaryButton={null}
-  secondaryButtonLabel="close"
-/>
-`;
-
-exports[`should render correctly: default 1`] = `
-<Gl
-  body={
-    <DeferredSpinner
-      loading={false}
-    >
-      <React.Fragment>
-        <div
-          className="sw-flex sw-items-center sw-mt-2"
-        >
-          <FlagMessage
-            variant="warning"
-          >
-            <HtmlFormatter>
-              <span
-                dangerouslySetInnerHTML={
-                  {
-                    "__html": "warning 1",
-                  }
-                }
-              />
-            </HtmlFormatter>
-          </FlagMessage>
-        </div>
-        <div />
-      </React.Fragment>
-      <React.Fragment>
-        <div
-          className="sw-flex sw-items-center sw-mt-2"
-        >
-          <FlagMessage
-            variant="warning"
-          >
-            <HtmlFormatter>
-              <span
-                dangerouslySetInnerHTML={
-                  {
-                    "__html": "warning 2",
-                  }
-                }
-              />
-            </HtmlFormatter>
-          </FlagMessage>
-        </div>
-        <div />
-      </React.Fragment>
-    </DeferredSpinner>
-  }
-  headerTitle="warnings"
-  onClose={[MockFunction]}
-  primaryButton={null}
-  secondaryButtonLabel="close"
-/>
-`;
-
-exports[`should render correctly: do not show dismissable links for anonymous 1`] = `
-<Gl
-  body={
-    <DeferredSpinner
-      loading={false}
-    >
-      <React.Fragment>
-        <div
-          className="sw-flex sw-items-center sw-mt-2"
-        >
-          <FlagMessage
-            variant="warning"
-          >
-            <HtmlFormatter>
-              <span
-                dangerouslySetInnerHTML={
-                  {
-                    "__html": "Lorem ipsum",
-                  }
-                }
-              />
-            </HtmlFormatter>
-          </FlagMessage>
-        </div>
-        <div />
-      </React.Fragment>
-    </DeferredSpinner>
-  }
-  headerTitle="warnings"
-  onClose={[MockFunction]}
-  primaryButton={null}
-  secondaryButtonLabel="close"
-/>
-`;
-
-exports[`should render correctly: with dismissable warnings 1`] = `
-<Gl
-  body={
-    <DeferredSpinner
-      loading={false}
-    >
-      <React.Fragment>
-        <div
-          className="sw-flex sw-items-center sw-mt-2"
-        >
-          <FlagMessage
-            variant="warning"
-          >
-            <HtmlFormatter>
-              <span
-                dangerouslySetInnerHTML={
-                  {
-                    "__html": "Lorem ipsum",
-                  }
-                }
-              />
-            </HtmlFormatter>
-          </FlagMessage>
-        </div>
-        <div>
-          <div
-            className="sw-mt-4"
-          >
-            <DangerButtonSecondary
-              disabled={false}
-              onClick={[Function]}
-            >
-              dismiss_permanently
-            </DangerButtonSecondary>
-            <DeferredSpinner
-              className="sw-ml-2"
-              loading={false}
-            />
-          </div>
-        </div>
-      </React.Fragment>
-    </DeferredSpinner>
-  }
-  headerTitle="warnings"
-  onClose={[MockFunction]}
-  primaryButton={null}
-  secondaryButtonLabel="close"
-/>
-`;
index 952979cba94afe9c134e1f5b1f4e0f5150d57b49..b9b7e58c7bdac186fc5e3f17e0a04c26e6886c03 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+
 import classNames from 'classnames';
 import { ToggleButton } from 'design-system';
 import { cloneDeep, debounce, groupBy } from 'lodash';
@@ -84,7 +85,9 @@ export class IssueTabViewer extends React.PureComponent<IssueTabViewerProps, Sta
 
   constructor(props: IssueTabViewerProps) {
     super(props);
+
     this.educationPrinciplesRef = React.createRef();
+
     this.checkIfEducationPrinciplesAreVisible = debounce(
       this.checkIfEducationPrinciplesAreVisible,
       DEBOUNCE_FOR_SCROLL
@@ -98,6 +101,7 @@ export class IssueTabViewer extends React.PureComponent<IssueTabViewerProps, Sta
     const tabs = this.computeTabs(Boolean(this.state.displayEducationalPrinciplesNotification));
 
     const query = new URLSearchParams(this.props.location.search);
+
     if (query.has('why')) {
       this.setState({
         selectedTab: tabs.find((tab) => tab.key === TabKeys.WhyIsThisAnIssue) || tabs[0],
@@ -114,6 +118,7 @@ export class IssueTabViewer extends React.PureComponent<IssueTabViewerProps, Sta
       selectedFlowIndex,
       selectedLocationIndex,
     } = this.props;
+
     const { selectedTab } = this.state;
 
     if (
@@ -163,6 +168,7 @@ export class IssueTabViewer extends React.PureComponent<IssueTabViewerProps, Sta
       ruleDetails.educationPrinciples.length > 0 &&
       isLoggedIn &&
       !dismissedNotices[NoticeType.EDUCATION_PRINCIPLES];
+
     const tabs = this.computeTabs(displayEducationalPrinciplesNotification);
 
     return {
@@ -175,7 +181,7 @@ export class IssueTabViewer extends React.PureComponent<IssueTabViewerProps, Sta
   computeTabs = (displayEducationalPrinciplesNotification: boolean) => {
     const {
       codeTabContent,
-      ruleDetails: { descriptionSections, educationPrinciples, type: ruleType },
+      ruleDetails: { descriptionSections, educationPrinciples, lang: ruleLanguage, type: ruleType },
       ruleDescriptionContextKey,
       extendedDescription,
       activityTabContent,
@@ -214,11 +220,12 @@ export class IssueTabViewer extends React.PureComponent<IssueTabViewerProps, Sta
         content: (descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] ||
           descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE]) && (
           <RuleDescription
+            defaultContextKey={ruleDescriptionContextKey}
+            language={ruleLanguage}
             sections={
               descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] ||
               descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE]
             }
-            defaultContextKey={ruleDescriptionContextKey}
           />
         ),
       },
@@ -228,6 +235,7 @@ export class IssueTabViewer extends React.PureComponent<IssueTabViewerProps, Sta
         label: translate('coding_rules.description_section.title', TabKeys.AssessTheIssue),
         content: descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM] && (
           <RuleDescription
+            language={ruleLanguage}
             sections={descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM]}
           />
         ),
@@ -238,8 +246,9 @@ export class IssueTabViewer extends React.PureComponent<IssueTabViewerProps, Sta
         label: translate('coding_rules.description_section.title', TabKeys.HowToFixIt),
         content: descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX] && (
           <RuleDescription
-            sections={descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX]}
             defaultContextKey={ruleDescriptionContextKey}
+            language={ruleLanguage}
+            sections={descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX]}
           />
         ),
       },
@@ -257,10 +266,11 @@ export class IssueTabViewer extends React.PureComponent<IssueTabViewerProps, Sta
         content: ((educationPrinciples && educationPrinciples.length > 0) ||
           descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]) && (
           <MoreInfoRuleDescription
-            educationPrinciples={educationPrinciples}
-            sections={descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]}
             displayEducationalPrinciplesNotification={displayEducationalPrinciplesNotification}
+            educationPrinciples={educationPrinciples}
             educationPrinciplesRef={this.educationPrinciplesRef}
+            language={ruleLanguage}
+            sections={descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]}
           />
         ),
         counter: displayEducationalPrinciplesNotification ? 1 : undefined,
@@ -346,7 +356,8 @@ export class IssueTabViewer extends React.PureComponent<IssueTabViewerProps, Sta
           {({ top }) => (
             <div
               style={{
-                // We substract the footer height with padding (80) and the main layout padding (20) and the tabs padding (20)
+                // We substract the footer height with padding (80) and the main layout padding (20)
+                // and the tabs padding (20)
                 maxHeight: scrollInTab ? `calc(100vh - ${top + 120}px)` : 'initial',
               }}
               className="sw-flex sw-flex-col"
index d134501fb5c63d856fb9273175ec8b01e7a42348..1b5f3e2521568ab7af7f866b48860b587c73c712 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+
 import * as React from 'react';
 import { RuleDescriptionSection } from '../../apps/coding-rules/rule';
 import { translate } from '../../helpers/l10n';
 import { Dict } from '../../types/types';
 import { ButtonLink } from '../controls/buttons';
 import { Alert } from '../ui/Alert';
+import RuleDescription from './RuleDescription';
 import DefenseInDepth from './educationPrinciples/DefenseInDepth';
 import NeverTrustUserInput from './educationPrinciples/NeverTrustUserInput';
-import RuleDescription from './RuleDescription';
 import './style.css';
 
 interface Props {
-  sections?: RuleDescriptionSection[];
-  educationPrinciples?: string[];
   displayEducationalPrinciplesNotification?: boolean;
+  educationPrinciples?: string[];
   educationPrinciplesRef?: React.RefObject<HTMLDivElement>;
+  language?: string;
+  sections?: RuleDescriptionSection[];
 }
 
 const EDUCATION_PRINCIPLES_MAP: Dict<React.ComponentType> = {
   defense_in_depth: DefenseInDepth,
   never_trust_user_input: NeverTrustUserInput,
 };
+
 export default class MoreInfoRuleDescription extends React.PureComponent<Props, {}> {
   handleNotificationScroll = () => {
     const element = this.props.educationPrinciplesRef?.current;
+
     if (element) {
       element.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
     }
@@ -50,10 +54,12 @@ export default class MoreInfoRuleDescription extends React.PureComponent<Props,
   render() {
     const {
       displayEducationalPrinciplesNotification,
+      language,
       sections = [],
       educationPrinciples = [],
       educationPrinciplesRef,
     } = this.props;
+
     return (
       <div className="padded rule-desc">
         {displayEducationalPrinciplesNotification && (
@@ -61,6 +67,7 @@ export default class MoreInfoRuleDescription extends React.PureComponent<Props,
             <p className="little-spacer-bottom little-spacer-top">
               {translate('coding_rules.more_info.notification_message')}
             </p>
+
             <ButtonLink
               onClick={() => {
                 this.handleNotificationScroll();
@@ -70,10 +77,11 @@ export default class MoreInfoRuleDescription extends React.PureComponent<Props,
             </ButtonLink>
           </Alert>
         )}
+
         {sections.length > 0 && (
           <>
             <h2>{translate('coding_rules.more_info.resources.title')}</h2>
-            <RuleDescription sections={sections} />
+            <RuleDescription language={language} sections={sections} />
           </>
         )}
 
@@ -82,11 +90,14 @@ export default class MoreInfoRuleDescription extends React.PureComponent<Props,
             <h2 ref={educationPrinciplesRef}>
               {translate('coding_rules.more_info.education_principles.title')}
             </h2>
+
             {educationPrinciples.map((key) => {
               const Concept = EDUCATION_PRINCIPLES_MAP[key];
+
               if (Concept === undefined) {
                 return null;
               }
+
               return (
                 <div key={key} className="education-principles big-spacer-top big-padded">
                   <Concept />
index 9f8a6d41a41bf7a9e75cb2618958fed479dd76bc..7cd177c3fa1017986d200d3540cb0b6740f368e8 100644 (file)
  * 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 { FlagMessage, HtmlFormatter, themeBorder, themeColor, ToggleButton } from 'design-system';
+import {
+  CodeSyntaxHighlighter,
+  FlagMessage,
+  HtmlFormatter,
+  ToggleButton,
+  themeBorder,
+  themeColor,
+} from 'design-system';
 import * as React from 'react';
 import { RuleDescriptionSection } from '../../apps/coding-rules/rule';
 import applyCodeDifferences from '../../helpers/code-difference';
@@ -29,9 +37,10 @@ import OtherContextOption from './OtherContextOption';
 const OTHERS_KEY = 'others';
 
 interface Props {
-  sections: RuleDescriptionSection[];
-  defaultContextKey?: string;
   className?: string;
+  defaultContextKey?: string;
+  language?: string;
+  sections: RuleDescriptionSection[];
 }
 
 interface State {
@@ -102,13 +111,14 @@ export default class RuleDescription extends React.PureComponent<Props, State> {
     const { contexts } = this.state;
 
     const selected = contexts.find((ctxt) => ctxt.displayName === value);
+
     if (selected) {
       this.setState({ selectedContext: selected });
     }
   };
 
   render() {
-    const { className, sections } = this.props;
+    const { className, language, sections } = this.props;
     const { contexts, defaultContext, selectedContext } = this.state;
 
     const options = contexts.map((ctxt) => ({
@@ -127,6 +137,7 @@ export default class RuleDescription extends React.PureComponent<Props, State> {
           <h2 className="sw-body-sm-highlight sw-mb-4">
             {translate('coding_rules.description_context.title')}
           </h2>
+
           {defaultContext && (
             <FlagMessage variant="info" className="sw-mb-4">
               {translateWithParameters(
@@ -135,6 +146,7 @@ export default class RuleDescription extends React.PureComponent<Props, State> {
               )}
             </FlagMessage>
           )}
+
           <div className="sw-mb-4">
             <ToggleButton
               label={translate('coding_rules.description_context.title')}
@@ -142,6 +154,7 @@ export default class RuleDescription extends React.PureComponent<Props, State> {
               options={options}
               value={selectedContext.displayName}
             />
+
             {selectedContext.key !== OTHERS_KEY && (
               <h2>
                 {translateWithParameters(
@@ -151,12 +164,13 @@ export default class RuleDescription extends React.PureComponent<Props, State> {
               </h2>
             )}
           </div>
+
           {selectedContext.key === OTHERS_KEY ? (
             <OtherContextOption />
           ) : (
-            <div
-              /* eslint-disable-next-line react/no-danger */
-              dangerouslySetInnerHTML={{ __html: sanitizeString(selectedContext.content) }}
+            <CodeSyntaxHighlighter
+              htmlAsString={sanitizeString(selectedContext.content)}
+              language={language}
             />
           )}
         </StyledHtmlFormatter>
@@ -169,11 +183,12 @@ export default class RuleDescription extends React.PureComponent<Props, State> {
         ref={(node: HTMLDivElement) => {
           applyCodeDifferences(node);
         }}
-        // eslint-disable-next-line react/no-danger
-        dangerouslySetInnerHTML={{
-          __html: sanitizeString(sections[0].content),
-        }}
-      />
+      >
+        <CodeSyntaxHighlighter
+          htmlAsString={sanitizeString(sections[0].content)}
+          language={language}
+        />
+      </StyledHtmlFormatter>
     );
   }
 }
index 33528e4f61c24a0c13bf701fad1a347a1a9b1dad..fa7ea48c6b9401acbc48db01d05ec3e592345fb8 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+
 import classNames from 'classnames';
 import { cloneDeep, debounce, groupBy } from 'lodash';
 import * as React from 'react';
@@ -82,7 +83,9 @@ export class RuleTabViewer extends React.PureComponent<RuleTabViewerProps, State
 
   constructor(props: RuleTabViewerProps) {
     super(props);
+
     this.educationPrinciplesRef = React.createRef();
+
     this.checkIfEducationPrinciplesAreVisible = debounce(
       this.checkIfEducationPrinciplesAreVisible,
       DEBOUNCE_FOR_SCROLL
@@ -96,6 +99,7 @@ export class RuleTabViewer extends React.PureComponent<RuleTabViewerProps, State
     const tabs = this.computeTabs(Boolean(this.state.displayEducationalPrinciplesNotification));
 
     const query = new URLSearchParams(this.props.location.search);
+
     if (query.has('why')) {
       this.setState({
         selectedTab: tabs.find((tab) => tab.key === TabKeys.WhyIsThisAnIssue) ?? tabs[0],
@@ -112,6 +116,7 @@ export class RuleTabViewer extends React.PureComponent<RuleTabViewerProps, State
       selectedFlowIndex,
       selectedLocationIndex,
     } = this.props;
+
     const { selectedTab } = this.state;
 
     if (
@@ -161,6 +166,7 @@ export class RuleTabViewer extends React.PureComponent<RuleTabViewerProps, State
       ruleDetails.educationPrinciples.length > 0 &&
       isLoggedIn &&
       !dismissedNotices[NoticeType.EDUCATION_PRINCIPLES];
+
     const tabs = this.computeTabs(displayEducationalPrinciplesNotification);
 
     return {
@@ -173,7 +179,7 @@ export class RuleTabViewer extends React.PureComponent<RuleTabViewerProps, State
   computeTabs = (displayEducationalPrinciplesNotification: boolean) => {
     const {
       codeTabContent,
-      ruleDetails: { descriptionSections, educationPrinciples, type: ruleType },
+      ruleDetails: { descriptionSections, educationPrinciples, lang: ruleLanguage, type: ruleType },
       ruleDescriptionContextKey,
       extendedDescription,
       activityTabContent,
@@ -193,8 +199,8 @@ export class RuleTabViewer extends React.PureComponent<RuleTabViewerProps, State
       } else {
         descriptionSectionsByKey[RuleDescriptionSections.RESOURCES] = [
           {
-            key: RuleDescriptionSections.RESOURCES,
             content: extendedDescription,
+            key: RuleDescriptionSections.RESOURCES,
           },
         ];
       }
@@ -202,74 +208,78 @@ export class RuleTabViewer extends React.PureComponent<RuleTabViewerProps, State
 
     const tabs: Tab[] = [
       {
-        key: TabKeys.WhyIsThisAnIssue,
-        label:
-          ruleType === 'SECURITY_HOTSPOT'
-            ? translate('coding_rules.description_section.title.root_cause.SECURITY_HOTSPOT')
-            : translate('coding_rules.description_section.title.root_cause'),
         content: (descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] ||
           descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE]) && (
           <RuleDescription
             className="padded"
+            defaultContextKey={ruleDescriptionContextKey}
+            language={ruleLanguage}
             sections={
               descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] ||
               descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE]
             }
-            defaultContextKey={ruleDescriptionContextKey}
           />
         ),
+        key: TabKeys.WhyIsThisAnIssue,
+        label:
+          ruleType === 'SECURITY_HOTSPOT'
+            ? translate('coding_rules.description_section.title.root_cause.SECURITY_HOTSPOT')
+            : translate('coding_rules.description_section.title.root_cause'),
       },
       {
-        key: TabKeys.AssessTheIssue,
-        label: translate('coding_rules.description_section.title', TabKeys.AssessTheIssue),
         content: descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM] && (
           <RuleDescription
             className="padded"
+            language={ruleLanguage}
             sections={descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM]}
           />
         ),
+        key: TabKeys.AssessTheIssue,
+        label: translate('coding_rules.description_section.title', TabKeys.AssessTheIssue),
       },
       {
-        key: TabKeys.HowToFixIt,
-        label: translate('coding_rules.description_section.title', TabKeys.HowToFixIt),
         content: descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX] && (
           <RuleDescription
             className="padded"
-            sections={descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX]}
             defaultContextKey={ruleDescriptionContextKey}
+            language={ruleLanguage}
+            sections={descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX]}
           />
         ),
+        key: TabKeys.HowToFixIt,
+        label: translate('coding_rules.description_section.title', TabKeys.HowToFixIt),
       },
       {
+        content: activityTabContent,
         key: TabKeys.Activity,
         label: translate('coding_rules.description_section.title', TabKeys.Activity),
-        content: activityTabContent,
       },
       {
-        key: TabKeys.MoreInfo,
-        label: (
-          <>
-            {translate('coding_rules.description_section.title', TabKeys.MoreInfo)}
-            {displayEducationalPrinciplesNotification && <div className="notice-dot" />}
-          </>
-        ),
         content: ((educationPrinciples && educationPrinciples.length > 0) ||
           descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]) && (
           <MoreInfoRuleDescription
-            educationPrinciples={educationPrinciples}
-            sections={descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]}
             displayEducationalPrinciplesNotification={displayEducationalPrinciplesNotification}
+            educationPrinciples={educationPrinciples}
             educationPrinciplesRef={this.educationPrinciplesRef}
+            language={ruleLanguage}
+            sections={descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]}
           />
         ),
+        key: TabKeys.MoreInfo,
+        label: (
+          <>
+            {translate('coding_rules.description_section.title', TabKeys.MoreInfo)}
+            {displayEducationalPrinciplesNotification && <div className="notice-dot" />}
+          </>
+        ),
       },
     ];
 
     if (codeTabContent !== undefined) {
       tabs.unshift({
+        content: codeTabContent,
         key: TabKeys.Code,
         label: translate('issue.tabs', TabKeys.Code),
-        content: codeTabContent,
       });
     }
 
@@ -342,14 +352,14 @@ export class RuleTabViewer extends React.PureComponent<RuleTabViewerProps, State
         <ScreenPositionHelper>
           {({ top }) => (
             <div
+              aria-labelledby={getTabId(selectedTab.key)}
+              className="bordered display-flex-column"
+              id={getTabPanelId(selectedTab.key)}
+              role="tabpanel"
               style={{
                 // We substract the footer height with padding (80) and the main layout padding (20)
                 maxHeight: scrollInTab ? `calc(100vh - ${top + 100}px)` : 'initial',
               }}
-              className="bordered display-flex-column"
-              role="tabpanel"
-              aria-labelledby={getTabId(selectedTab.key)}
-              id={getTabPanelId(selectedTab.key)}
             >
               {
                 // Preserve tabs state by always rendering all of them. Only hide them when not selected
index 96974fb3ca3282f11fe2efc9c166e8267f70d507..b15212b8d6503b7d714a19deaefbd8c16b055c8f 100644 (file)
@@ -17,6 +17,8 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+
+import { uniqueId } from 'lodash';
 import { ComponentQualifier } from '../../types/component';
 import { Task, TaskStatuses, TaskTypes, TaskWarning } from '../../types/tasks';
 
@@ -36,7 +38,7 @@ export function mockTask(overrides: Partial<Task> = {}): Task {
 
 export function mockTaskWarning(overrides: Partial<TaskWarning> = {}): TaskWarning {
   return {
-    key: 'foo',
+    key: uniqueId('foo'),
     message: 'Lorem ipsum',
     dismissable: false,
     ...overrides,
index 0caa169d4e66a993f133fed0b779f2d2158d3774..5c9ee44f84de4a88cce79c1523bd33012dad96af 100644 (file)
@@ -6173,6 +6173,9 @@ __metadata:
     eslint-plugin-local-rules: 1.3.2
     eslint-plugin-typescript-sort-keys: 2.3.0
     highlight.js: 11.7.0
+    highlightjs-apex: 1.2.0
+    highlightjs-cobol: 0.3.3
+    highlightjs-sap-abap: 0.2.0
     history: 5.3.0
     jest: 29.5.0
     postcss: 8.4.21
@@ -7877,6 +7880,30 @@ __metadata:
   languageName: node
   linkType: hard
 
+"highlightjs-apex@npm:1.2.0":
+  version: 1.2.0
+  resolution: "highlightjs-apex@npm:1.2.0"
+  checksum: d0e1543dbdfa156c0bb6da74afcb7aaf315fe211024246917c8f967075d6c239bcd408d6a667c324ea75b928a2d8495cb1b8d160b403894aea8c9a16c293f33a
+  languageName: node
+  linkType: hard
+
+"highlightjs-cobol@npm:0.3.3":
+  version: 0.3.3
+  resolution: "highlightjs-cobol@npm:0.3.3"
+  dependencies:
+    minimist: ">=1.2.6"
+    mkdirp: ^1.0.4
+  checksum: f59a694703f883ead2fbdf262f36eab583c8bbf64649e59d5de1e13a43e43979ad9030eab69e1cb08f7558ce60ca5cfd1fe42c7a38e34fd8b0baebf06df9118d
+  languageName: node
+  linkType: hard
+
+"highlightjs-sap-abap@npm:0.2.0":
+  version: 0.2.0
+  resolution: "highlightjs-sap-abap@npm:0.2.0"
+  checksum: 685293fc2de3b333d0166f7323d38b53b70a53a462851d075484826c7ee16fa09f4c24cda44c1c957525352e112ba0a83cefe37641ee65b62e1483be93c60019
+  languageName: node
+  linkType: hard
+
 "history@npm:5.3.0":
   version: 5.3.0
   resolution: "history@npm:5.3.0"
@@ -9816,6 +9843,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"minimist@npm:>=1.2.6":
+  version: 1.2.8
+  resolution: "minimist@npm:1.2.8"
+  checksum: 75a6d645fb122dad29c06a7597bddea977258957ed88d7a6df59b5cd3fe4a527e253e9bbf2e783e4b73657f9098b96a5fe96ab8a113655d4109108577ecf85b0
+  languageName: node
+  linkType: hard
+
 "minimist@npm:^1.2.0, minimist@npm:^1.2.5":
   version: 1.2.5
   resolution: "minimist@npm:1.2.5"