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 30f57f506a63dee24ffffd8ec8f74924b9fa0dc1..e2d1a81ee2bd8422fe42aefa96d375d775882071 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
@@ -146,6 +146,3 @@ export const BIDI_CHAR_TOOLTIP = __(
export const HLJS_COMMENT_SELECTOR = 'hljs-comment';
export const HLJS_ON_AFTER_HIGHLIGHT = 'after:highlight';
-
-export const NPM_URL = 'https://npmjs.com/package';
-export const GEM_URL = 'https://rubygems.org/gems';
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util.js
index dbe6812cf16d15e4f6aca2632b4ec983d860296e..49704421d6eaed7b26f3b904f6db75b177560dcf 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util.js
@@ -1,16 +1,7 @@
import { escape } from 'lodash';
-import { setAttributes } from '~/lib/utils/dom_utils';
-export const createLink = (href, innerText) => {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- const rel = 'nofollow noreferrer noopener';
- const link = document.createElement('a');
-
- setAttributes(link, { href: escape(href), rel });
- link.textContent = innerText;
-
- return link.outerHTML;
-};
+export const createLink = (href, innerText) =>
+ `${escape(innerText)}`;
export const generateHLJSOpenTag = (type, delimiter = '"') =>
`${delimiter}`;
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/gemspec_linker.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/gemspec_linker.js
index 35de8fd13d62fce2d7c694c7cb26fa6206b569f9..46c9dc38300a4b8ab96bc01a4ba5fef447aa323c 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/gemspec_linker.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/gemspec_linker.js
@@ -1,7 +1,6 @@
-import { joinPaths } from '~/lib/utils/url_utility';
-import { GEM_URL } from '../../constants';
import { createLink, generateHLJSOpenTag } from './dependency_linker_util';
+const GEM_URL = 'https://rubygems.org/gems/';
const methodRegex = '.*add_dependency.*|.*add_runtime_dependency.*|.*add_development_dependency.*';
const openTagRegex = generateHLJSOpenTag('string', '(&.*;)');
const closeTagRegex = '&.*';
@@ -24,7 +23,7 @@ const DEPENDENCY_REGEX = new RegExp(
const handleReplace = (method, delimiter, packageName, closeTag, rest) => {
// eslint-disable-next-line @gitlab/require-i18n-strings
const openTag = generateHLJSOpenTag('string linked', delimiter);
- const href = joinPaths(GEM_URL, packageName);
+ const href = `${GEM_URL}${packageName}`;
const packageLink = createLink(href, packageName);
return `${method}${openTag}${packageLink}${closeTag}${rest}`;
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/package_json_linker.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/package_json_linker.js
index 3c6fc23c138cedb922f37ec5642401fa14b2d775..4bfd5ec2431da13c445b4660c2b360bde2b4517f 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/package_json_linker.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/package_json_linker.js
@@ -1,8 +1,7 @@
import { unescape } from 'lodash';
-import { joinPaths } from '~/lib/utils/url_utility';
-import { NPM_URL } from '../../constants';
import { createLink, generateHLJSOpenTag } from './dependency_linker_util';
+const NPM_URL = 'https://npmjs.com/package/';
const attrOpenTag = generateHLJSOpenTag('attr');
const stringOpenTag = generateHLJSOpenTag('string');
const closeTag = '"';
@@ -20,7 +19,7 @@ const DEPENDENCY_REGEX = new RegExp(
const handleReplace = (original, packageName, version, dependenciesToLink) => {
const unescapedPackageName = unescape(packageName);
const unescapedVersion = unescape(version);
- const href = joinPaths(NPM_URL, unescapedPackageName);
+ const href = `${NPM_URL}${unescapedPackageName}`;
const packageLink = createLink(href, unescapedPackageName);
const versionLink = createLink(href, unescapedVersion);
const closeAndOpenTag = `${closeTag}: ${attrOpenTag}`;
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight.js b/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight.js
new file mode 100644
index 0000000000000000000000000000000000000000..535e857d7a94b56a024a7516493b3fa9f34d16cf
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight.js
@@ -0,0 +1,10 @@
+import { highlight } from './highlight_utils';
+
+/**
+ * A webworker for highlighting large amounts of content with Highlight.js
+ */
+// eslint-disable-next-line no-restricted-globals
+self.addEventListener('message', ({ data: { fileType, content, language } }) => {
+ // eslint-disable-next-line no-restricted-globals
+ self.postMessage(highlight(fileType, content, language));
+});
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js b/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js
new file mode 100644
index 0000000000000000000000000000000000000000..0da57f9e6fa4175721a553574e367347345f9b1a
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js
@@ -0,0 +1,15 @@
+import hljs from 'highlight.js/lib/core';
+import languageLoader from '~/content_editor/services/highlight_js_language_loader';
+import { registerPlugins } from '../plugins/index';
+
+const initHighlightJs = async (fileType, content, language) => {
+ const languageDefinition = await languageLoader[language]();
+
+ registerPlugins(hljs, fileType, content);
+ hljs.registerLanguage(language, languageDefinition.default);
+};
+
+export const highlight = (fileType, content, language) => {
+ initHighlightJs(fileType, content, language);
+ return hljs.highlight(content, { language }).value;
+};
diff --git a/spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js b/spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..4a995e2fde16bc86547d339bdd3b98839096a1ef
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js
@@ -0,0 +1,44 @@
+import hljs from 'highlight.js/lib/core';
+import languageLoader from '~/content_editor/services/highlight_js_language_loader';
+import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index';
+import { highlight } from '~/vue_shared/components/source_viewer/workers/highlight_utils';
+
+jest.mock('highlight.js/lib/core', () => ({
+ highlight: jest.fn().mockReturnValue({}),
+ registerLanguage: jest.fn(),
+}));
+
+jest.mock('~/content_editor/services/highlight_js_language_loader', () => ({
+ javascript: jest.fn().mockReturnValue({ default: jest.fn() }),
+}));
+
+jest.mock('~/vue_shared/components/source_viewer/plugins/index', () => ({
+ registerPlugins: jest.fn(),
+}));
+
+const fileType = 'text';
+const content = 'function test() { return true };';
+const language = 'javascript';
+
+describe('Highlight utility', () => {
+ beforeEach(() => highlight(fileType, content, language));
+
+ it('loads the language', () => {
+ expect(languageLoader.javascript).toHaveBeenCalled();
+ });
+
+ it('registers the plugins', () => {
+ expect(registerPlugins).toHaveBeenCalled();
+ });
+
+ it('registers the language', () => {
+ expect(hljs.registerLanguage).toHaveBeenCalledWith(
+ language,
+ languageLoader[language]().default,
+ );
+ });
+
+ it('highlights the content', () => {
+ expect(hljs.highlight).toHaveBeenCalledWith(content, { language });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js
index 8079d5ad99a30e277b781bb549ce857313058601..e4ce07ec668eed61d40248b8df21a9880927b773 100644
--- a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js
@@ -15,7 +15,7 @@ describe('createLink', () => {
it('escapes the user-controlled content', () => {
const unescapedXSS = '';
const escapedPackageName = '<script>XSS</script>';
- const escapedHref = '<script>XSS</script>';
+ const escapedHref = '<script>XSS</script>';
const href = `http://test.com/${unescapedXSS}`;
const innerText = `testing${unescapedXSS}`;
const result = `testing${escapedPackageName}`;