diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue
index 257b9f57222031387f8a949d1c510e76b2466c61..6395dbdac5069bd2e784f2000ccb618a25448d67 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue
@@ -1,8 +1,6 @@
@@ -79,6 +49,6 @@ export default {
+ >
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
index e2d1a81ee2bd8422fe42aefa96d375d775882071..10489a4eff3dcbea38c7b6e71a850c835f1a0d73 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
@@ -1,5 +1,3 @@
-import { __ } from '~/locale';
-
// Language map from Rouge::Lexer to highlight.js
// Rouge::Lexer - We use it on the BE to determine the language of a source file (https://github.com/rouge-ruby/rouge/blob/master/docs/Languages.md).
// Highlight.js - We use it on the FE to highlight the syntax of a source file (https://github.com/highlightjs/highlight.js/tree/main/src/languages).
@@ -139,9 +137,7 @@ export const BIDI_CHARS = [
export const BIDI_CHARS_CLASS_LIST = 'unicode-bidi has-tooltip';
-export const BIDI_CHAR_TOOLTIP = __(
- 'Potentially unwanted character detected: Unicode BiDi Control',
-);
+export const BIDI_CHAR_TOOLTIP = 'Potentially unwanted character detected: Unicode BiDi Control';
export const HLJS_COMMENT_SELECTOR = 'hljs-comment';
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js
index 5d24a3d110bbb2c7bcfa7d24d196b486c0f7702c..e38c9b8f66faf896d22e9d767d1179f7c1f67984 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js
@@ -1,6 +1,8 @@
-import { HLJS_ON_AFTER_HIGHLIGHT } from '../constants';
import wrapComments from './wrap_comments';
import linkDependencies from './link_dependencies';
+import wrapBidiChars from './wrap_bidi_chars';
+
+export const HLJS_ON_AFTER_HIGHLIGHT = 'after:highlight';
/**
* Registers our plugins for Highlight.js
@@ -10,6 +12,7 @@ import linkDependencies from './link_dependencies';
* @param {Object} hljs - the Highlight.js instance.
*/
export const registerPlugins = (hljs, fileType, rawContent) => {
+ hljs.addPlugin({ [HLJS_ON_AFTER_HIGHLIGHT]: wrapBidiChars });
hljs.addPlugin({ [HLJS_ON_AFTER_HIGHLIGHT]: wrapComments });
hljs.addPlugin({
[HLJS_ON_AFTER_HIGHLIGHT]: (result) => linkDependencies(result, fileType, rawContent),
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_bidi_chars.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_bidi_chars.js
new file mode 100644
index 0000000000000000000000000000000000000000..3b6cd96ef78253d93788f442e6be3a66df26b692
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_bidi_chars.js
@@ -0,0 +1,30 @@
+import {
+ BIDI_CHARS,
+ BIDI_CHARS_CLASS_LIST,
+ BIDI_CHAR_TOOLTIP,
+} from '~/vue_shared/components/source_viewer/constants';
+
+/**
+ * Highlight.js plugin for wrapping BIDI chars.
+ * This ensures potentially dangerous BIDI characters are highlighted.
+ *
+ * Plugin API: https://github.com/highlightjs/highlight.js/blob/main/docs/plugin-api.rst
+ *
+ * @param {Object} Result - an object that represents the highlighted result from Highlight.js
+ */
+
+function wrapBidiChar(bidiChar) {
+ return `${bidiChar}`;
+}
+
+export default (result) => {
+ let { value } = result;
+ BIDI_CHARS.forEach((bidiChar) => {
+ if (value.includes(bidiChar)) {
+ value = value.replace(bidiChar, wrapBidiChar(bidiChar));
+ }
+ });
+
+ // eslint-disable-next-line no-param-reassign
+ result.value = value; // Highlight.js expects the result param to be mutated for plugins to work
+};
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js
index fd3ff9ce892efe36714755ed554e98a1b67786c6..f661bd6747a531b321b105080528bcb2818d1d5d 100644
--- a/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js
@@ -1,10 +1,5 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ChunkLine from '~/vue_shared/components/source_viewer/components/chunk_line.vue';
-import {
- BIDI_CHARS,
- BIDI_CHARS_CLASS_LIST,
- BIDI_CHAR_TOOLTIP,
-} from '~/vue_shared/components/source_viewer/constants';
const DEFAULT_PROPS = {
number: 2,
@@ -31,7 +26,6 @@ describe('Chunk Line component', () => {
const findLineLink = () => wrapper.find('.file-line-num');
const findBlameLink = () => wrapper.find('.file-line-blame');
const findContent = () => wrapper.findByTestId('content');
- const findWrappedBidiChars = () => wrapper.findAllByTestId('bidi-wrapper');
beforeEach(() => {
createComponent();
@@ -40,22 +34,6 @@ describe('Chunk Line component', () => {
afterEach(() => wrapper.destroy());
describe('rendering', () => {
- it('wraps BiDi characters', () => {
- const content = `// some content ${BIDI_CHARS.toString()} with BiDi chars`;
- createComponent({ content });
- const wrappedBidiChars = findWrappedBidiChars();
-
- expect(wrappedBidiChars.length).toBe(BIDI_CHARS.length);
-
- wrappedBidiChars.wrappers.forEach((_, i) => {
- expect(wrappedBidiChars.at(i).text()).toBe(BIDI_CHARS[i]);
- expect(wrappedBidiChars.at(i).attributes()).toMatchObject({
- class: BIDI_CHARS_CLASS_LIST,
- title: BIDI_CHAR_TOOLTIP,
- });
- });
- });
-
it('renders a blame link', () => {
expect(findBlameLink().attributes()).toMatchObject({
href: `${DEFAULT_PROPS.blamePath}#L${DEFAULT_PROPS.number}`,
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/index_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/index_spec.js
index 83fdc5d669d5cec771f3755f7c159a28a1b54b4b..dc9954dcc627c050cb9060fba92d3defae884954 100644
--- a/spec/frontend/vue_shared/components/source_viewer/plugins/index_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/index_spec.js
@@ -1,6 +1,9 @@
-import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index';
-import { HLJS_ON_AFTER_HIGHLIGHT } from '~/vue_shared/components/source_viewer/constants';
+import {
+ registerPlugins,
+ HLJS_ON_AFTER_HIGHLIGHT,
+} from '~/vue_shared/components/source_viewer/plugins/index';
import wrapComments from '~/vue_shared/components/source_viewer/plugins/wrap_comments';
+import wrapBidiChars from '~/vue_shared/components/source_viewer/plugins/wrap_bidi_chars';
jest.mock('~/vue_shared/components/source_viewer/plugins/wrap_comments');
const hljsMock = { addPlugin: jest.fn() };
@@ -9,6 +12,7 @@ describe('Highlight.js plugin registration', () => {
beforeEach(() => registerPlugins(hljsMock));
it('registers our plugins', () => {
+ expect(hljsMock.addPlugin).toHaveBeenCalledWith({ [HLJS_ON_AFTER_HIGHLIGHT]: wrapBidiChars });
expect(hljsMock.addPlugin).toHaveBeenCalledWith({ [HLJS_ON_AFTER_HIGHLIGHT]: wrapComments });
});
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_bidi_chars_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_bidi_chars_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..f40f8b2262724efc37665141976764534fb43348
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_bidi_chars_spec.js
@@ -0,0 +1,17 @@
+import wrapBidiChars from '~/vue_shared/components/source_viewer/plugins/wrap_bidi_chars';
+import {
+ BIDI_CHARS,
+ BIDI_CHARS_CLASS_LIST,
+ BIDI_CHAR_TOOLTIP,
+} from '~/vue_shared/components/source_viewer/constants';
+
+describe('Highlight.js plugin for wrapping BiDi characters', () => {
+ it.each(BIDI_CHARS)('wraps %s BiDi char', (bidiChar) => {
+ const inputValue = `// some content ${bidiChar} with BiDi chars`;
+ const outputValue = `// some content ${bidiChar}`;
+ const hljsResultMock = { value: inputValue };
+
+ wrapBidiChars(hljsResultMock);
+ expect(hljsResultMock.value).toContain(outputValue);
+ });
+});