aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/design-system/src
diff options
context:
space:
mode:
authorLucas <97296331+lucas-paulger-sonarsource@users.noreply.github.com>2024-07-29 15:47:33 +0300
committersonartech <sonartech@sonarsource.com>2024-08-13 20:02:46 +0000
commit176045866b4d946f1ff764719d29024de10c261c (patch)
treed8c0514a939f905dc9187d632bf1bf9850993b67 /server/sonar-web/design-system/src
parent294afa38afda74f98f15f1edca798c67322502b8 (diff)
downloadsonarqube-176045866b4d946f1ff764719d29024de10c261c.tar.gz
sonarqube-176045866b4d946f1ff764719d29024de10c261c.zip
SONAR-22498 CodeSnippet supports issue indicator for jupyter preview (#11414)
Diffstat (limited to 'server/sonar-web/design-system/src')
-rw-r--r--server/sonar-web/design-system/src/components/CodeSyntaxHighlighter.tsx3
-rw-r--r--server/sonar-web/design-system/src/sonar-aligned/hljs/HljsIssueIndicatorPlugin.ts135
-rw-r--r--server/sonar-web/design-system/src/sonar-aligned/hljs/__tests__/HljsIssueIndicatorPlugin-test.ts126
-rw-r--r--server/sonar-web/design-system/src/sonar-aligned/hljs/index.ts2
4 files changed, 265 insertions, 1 deletions
diff --git a/server/sonar-web/design-system/src/components/CodeSyntaxHighlighter.tsx b/server/sonar-web/design-system/src/components/CodeSyntaxHighlighter.tsx
index 502cc4a9eae..1a35a36d10a 100644
--- a/server/sonar-web/design-system/src/components/CodeSyntaxHighlighter.tsx
+++ b/server/sonar-web/design-system/src/components/CodeSyntaxHighlighter.tsx
@@ -25,7 +25,7 @@ import cobol from 'highlightjs-cobol';
import abap from 'highlightjs-sap-abap';
import tw from 'twin.macro';
import { themeColor, themeContrast } from '../helpers/theme';
-import { hljsUnderlinePlugin } from '../sonar-aligned/hljs/HljsUnderlinePlugin';
+import { hljsIssueIndicatorPlugin, hljsUnderlinePlugin } from '../sonar-aligned';
hljs.registerLanguage('abap', abap);
hljs.registerLanguage('apex', apex);
@@ -39,6 +39,7 @@ hljs.registerAliases('secrets', { languageName: 'markdown' });
hljs.registerAliases('web', { languageName: 'xml' });
hljs.registerAliases(['cloudformation', 'kubernetes'], { languageName: 'yaml' });
+hljs.addPlugin(hljsIssueIndicatorPlugin);
hljs.addPlugin(hljsUnderlinePlugin);
interface Props {
diff --git a/server/sonar-web/design-system/src/sonar-aligned/hljs/HljsIssueIndicatorPlugin.ts b/server/sonar-web/design-system/src/sonar-aligned/hljs/HljsIssueIndicatorPlugin.ts
new file mode 100644
index 00000000000..08d32b3468e
--- /dev/null
+++ b/server/sonar-web/design-system/src/sonar-aligned/hljs/HljsIssueIndicatorPlugin.ts
@@ -0,0 +1,135 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 { BeforeHighlightContext, HighlightResult } from 'highlight.js';
+
+const BREAK_LINE_REGEXP = /\n/g;
+
+export class HljsIssueIndicatorPlugin {
+ static readonly LINE_WRAPPER_STYLE = [
+ 'display: inline-grid',
+ 'grid-template-rows: auto',
+ 'grid-template-columns: 26px 1fr',
+ 'align-items: center',
+ ].join(';');
+
+ private issueKeys: { [key: string]: string[] };
+ static readonly LINE_WRAPPER_OPEN_TAG = `<div style="${this.LINE_WRAPPER_STYLE}">`;
+ static readonly LINE_WRAPPER_CLOSE_TAG = `</div>`;
+ static readonly EMPTY_INDICATOR_COLUMN = `<div></div>`;
+ public lineIssueIndicatorElement(issueKey: string) {
+ return `<div id="issue-key-${issueKey}"></div>`;
+ }
+
+ constructor() {
+ this.issueKeys = {};
+ }
+
+ 'before:highlight'(data: BeforeHighlightContext) {
+ data.code = this.extractIssue(data.code);
+ }
+
+ 'after:highlight'(data: HighlightResult) {
+ if (Object.keys(this.issueKeys).length > 0) {
+ data.value = this.addIssueIndicator(data.value);
+ }
+ // reset issueKeys for next CodeSnippet
+ this.issueKeys = {};
+ }
+
+ addIssuesToLines = (sourceLines: string[], issues: { [line: number]: string[] }) => {
+ return sourceLines.map((line, lineIndex) => {
+ const issuesByLine = issues[lineIndex];
+ if (!issues || !issuesByLine) {
+ return line;
+ }
+
+ return `[ISSUE_KEYS:${issuesByLine.join(',')}]${line}`;
+ });
+ };
+
+ private getLines(text: string) {
+ if (text.length === 0) {
+ return [];
+ }
+ return text.split(BREAK_LINE_REGEXP);
+ }
+
+ private extractIssue(inputHtml: string) {
+ const lines = this.getLines(inputHtml);
+ const issueKeysPattern = /\[ISSUE_KEYS:([^\]]+)\](.+)/;
+ const removeIssueKeysPattern = /\[ISSUE_KEYS:[^\]]+\](.+)/;
+
+ const wrappedLines = lines.map((line, index) => {
+ const match = issueKeysPattern.exec(line);
+
+ if (match) {
+ const issueKeys = match[1].split(',');
+ if (!this.issueKeys[index]) {
+ this.issueKeys[index] = issueKeys;
+ } else {
+ this.issueKeys[index].push(...issueKeys);
+ }
+ }
+
+ const result = removeIssueKeysPattern.exec(line);
+
+ return result ? result[1] : line;
+ });
+
+ return wrappedLines.join('\n');
+ }
+
+ private addIssueIndicator(inputHtml: string) {
+ const lines = this.getLines(inputHtml);
+
+ const wrappedLines = lines.map((line, index) => {
+ const issueKeys = this.issueKeys[index];
+
+ if (issueKeys) {
+ // the react portal looks for the first issue key
+ const referenceIssueKey = issueKeys[0];
+ return [
+ HljsIssueIndicatorPlugin.LINE_WRAPPER_OPEN_TAG,
+ this.lineIssueIndicatorElement(referenceIssueKey),
+ '<div>',
+ line,
+ '</div>',
+ HljsIssueIndicatorPlugin.LINE_WRAPPER_CLOSE_TAG,
+ ].join('');
+ }
+
+ // Keep the correct structure when at least one line has issues
+ return [
+ HljsIssueIndicatorPlugin.LINE_WRAPPER_OPEN_TAG,
+ HljsIssueIndicatorPlugin.EMPTY_INDICATOR_COLUMN,
+ '<div>',
+ line,
+ '</div>',
+ HljsIssueIndicatorPlugin.LINE_WRAPPER_CLOSE_TAG,
+ ].join('');
+ });
+
+ return wrappedLines.join('\n');
+ }
+}
+
+const hljsIssueIndicatorPlugin = new HljsIssueIndicatorPlugin();
+export { hljsIssueIndicatorPlugin };
diff --git a/server/sonar-web/design-system/src/sonar-aligned/hljs/__tests__/HljsIssueIndicatorPlugin-test.ts b/server/sonar-web/design-system/src/sonar-aligned/hljs/__tests__/HljsIssueIndicatorPlugin-test.ts
new file mode 100644
index 00000000000..2760ee2c65b
--- /dev/null
+++ b/server/sonar-web/design-system/src/sonar-aligned/hljs/__tests__/HljsIssueIndicatorPlugin-test.ts
@@ -0,0 +1,126 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 { BeforeHighlightContext, HighlightResult } from 'highlight.js';
+import { hljsIssueIndicatorPlugin, HljsIssueIndicatorPlugin } from '../HljsIssueIndicatorPlugin';
+
+describe('HljsIssueIndicatorPlugin', () => {
+ it('should prepend to the line the issues that were found', () => {
+ expect(
+ hljsIssueIndicatorPlugin.addIssuesToLines(['line1', 'line2', 'line3', `line4`, 'line5'], {
+ 1: ['123abd', '234asd'],
+ }),
+ ).toEqual(['line1', '[ISSUE_KEYS:123abd,234asd]line2', 'line3', `line4`, 'line5']);
+
+ expect(
+ hljsIssueIndicatorPlugin.addIssuesToLines(['line1', 'line2', 'line3', `line4`, 'line5'], {
+ 1: ['123abd'],
+ }),
+ ).toEqual(['line1', '[ISSUE_KEYS:123abd]line2', 'line3', `line4`, 'line5']);
+ });
+ describe('when tokens exist in the code snippet', () => {
+ it('should indicate an issue on a line', () => {
+ const inputHtml = {
+ code: hljsIssueIndicatorPlugin
+ .addIssuesToLines(['line1', 'line2', 'line3', `line4`, 'line5'], { 1: ['123abd'] })
+ .join('\n'),
+ } as BeforeHighlightContext;
+ const result = {
+ value: ['line1', `line2`, 'line3', `line4`, 'line5'].join('\n'),
+ } as HighlightResult;
+
+ //find issue keys
+ hljsIssueIndicatorPlugin['before:highlight'](inputHtml);
+ //add the issue indicator html
+ hljsIssueIndicatorPlugin['after:highlight'](result);
+
+ expect(result.value).toEqual(
+ [
+ `${HljsIssueIndicatorPlugin.LINE_WRAPPER_OPEN_TAG}${HljsIssueIndicatorPlugin.EMPTY_INDICATOR_COLUMN}<div>line1</div>${HljsIssueIndicatorPlugin.LINE_WRAPPER_CLOSE_TAG}`,
+ `${HljsIssueIndicatorPlugin.LINE_WRAPPER_OPEN_TAG}<div id="issue-key-123abd"></div><div>line2</div>${HljsIssueIndicatorPlugin.LINE_WRAPPER_CLOSE_TAG}`,
+ `${HljsIssueIndicatorPlugin.LINE_WRAPPER_OPEN_TAG}${HljsIssueIndicatorPlugin.EMPTY_INDICATOR_COLUMN}<div>line3</div>${HljsIssueIndicatorPlugin.LINE_WRAPPER_CLOSE_TAG}`,
+ `${HljsIssueIndicatorPlugin.LINE_WRAPPER_OPEN_TAG}${HljsIssueIndicatorPlugin.EMPTY_INDICATOR_COLUMN}<div>line4</div>${HljsIssueIndicatorPlugin.LINE_WRAPPER_CLOSE_TAG}`,
+ `${HljsIssueIndicatorPlugin.LINE_WRAPPER_OPEN_TAG}${HljsIssueIndicatorPlugin.EMPTY_INDICATOR_COLUMN}<div>line5</div>${HljsIssueIndicatorPlugin.LINE_WRAPPER_CLOSE_TAG}`,
+ ].join('\n'),
+ );
+ });
+
+ it('should support multiple issues found on one line', () => {
+ const inputHtml = {
+ code: hljsIssueIndicatorPlugin
+ .addIssuesToLines(['line1', 'line2 issue2', 'line3', `line4`, 'line5'], {
+ 1: ['123abd', '234asd'],
+ })
+ .join('\n'),
+ } as BeforeHighlightContext;
+ const result = {
+ value: ['line1', `line2 issue2`, 'line3', `line4`, 'line5'].join('\n'),
+ } as HighlightResult;
+
+ //find issue keys
+ hljsIssueIndicatorPlugin['before:highlight'](inputHtml);
+ //add the issue indicator html
+ hljsIssueIndicatorPlugin['after:highlight'](result);
+
+ expect(result.value).toEqual(
+ [
+ `${HljsIssueIndicatorPlugin.LINE_WRAPPER_OPEN_TAG}${HljsIssueIndicatorPlugin.EMPTY_INDICATOR_COLUMN}<div>line1</div>${HljsIssueIndicatorPlugin.LINE_WRAPPER_CLOSE_TAG}`,
+ `${HljsIssueIndicatorPlugin.LINE_WRAPPER_OPEN_TAG}<div id="issue-key-123abd"></div><div>line2 issue2</div>${HljsIssueIndicatorPlugin.LINE_WRAPPER_CLOSE_TAG}`,
+ `${HljsIssueIndicatorPlugin.LINE_WRAPPER_OPEN_TAG}${HljsIssueIndicatorPlugin.EMPTY_INDICATOR_COLUMN}<div>line3</div>${HljsIssueIndicatorPlugin.LINE_WRAPPER_CLOSE_TAG}`,
+ `${HljsIssueIndicatorPlugin.LINE_WRAPPER_OPEN_TAG}${HljsIssueIndicatorPlugin.EMPTY_INDICATOR_COLUMN}<div>line4</div>${HljsIssueIndicatorPlugin.LINE_WRAPPER_CLOSE_TAG}`,
+ `${HljsIssueIndicatorPlugin.LINE_WRAPPER_OPEN_TAG}${HljsIssueIndicatorPlugin.EMPTY_INDICATOR_COLUMN}<div>line5</div>${HljsIssueIndicatorPlugin.LINE_WRAPPER_CLOSE_TAG}`,
+ ].join('\n'),
+ );
+ });
+
+ it('should not render anything if no source code is passed', () => {
+ const inputHtml = {
+ code: '',
+ } as BeforeHighlightContext;
+ const result = {
+ value: '',
+ } as HighlightResult;
+
+ //find issue keys
+ hljsIssueIndicatorPlugin['before:highlight'](inputHtml);
+ //add the issue indicator html
+ hljsIssueIndicatorPlugin['after:highlight'](result);
+
+ expect(result.value).toEqual('');
+ });
+ });
+
+ describe('when no tokens exist in the code snippet', () => {
+ it('should not change the source', () => {
+ const inputHtml = {
+ code: ['line1', `line2`, 'line3', `line4`, 'line5'].join('\n'),
+ } as BeforeHighlightContext;
+ const result = {
+ value: ['line1', `line2`, 'line3', `line4`, 'line5'].join('\n'),
+ } as HighlightResult;
+
+ //find issue keys
+ hljsIssueIndicatorPlugin['before:highlight'](inputHtml);
+ //add the issue indicator html
+ hljsIssueIndicatorPlugin['after:highlight'](result);
+
+ expect(result.value).toEqual(['line1', 'line2', 'line3', 'line4', 'line5'].join('\n'));
+ });
+ });
+});
diff --git a/server/sonar-web/design-system/src/sonar-aligned/hljs/index.ts b/server/sonar-web/design-system/src/sonar-aligned/hljs/index.ts
index b7394666777..0816564955e 100644
--- a/server/sonar-web/design-system/src/sonar-aligned/hljs/index.ts
+++ b/server/sonar-web/design-system/src/sonar-aligned/hljs/index.ts
@@ -17,4 +17,6 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
+export { hljsIssueIndicatorPlugin } from './HljsIssueIndicatorPlugin';
export { hljsUnderlinePlugin } from './HljsUnderlinePlugin';