summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/vue_shared/components/source_viewer
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/vue_shared/components/source_viewer')
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue34
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/constants.js11
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js9
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util.js13
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/gemspec_linker.js5
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/package_json_linker.js5
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_bidi_chars.js30
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_child_nodes.js45
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_comments.js41
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight.js10
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js15
12 files changed, 116 insertions, 104 deletions
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 257b9f57222..ffd0eea63a1 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 @@
<script>
import { GlSafeHtmlDirective } from '@gitlab/ui';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { setAttributes } from '~/lib/utils/dom_utils';
-import { BIDI_CHARS, BIDI_CHARS_CLASS_LIST, BIDI_CHAR_TOOLTIP } from '../constants';
export default {
directives: {
@@ -27,34 +25,6 @@ export default {
required: true,
},
},
- computed: {
- formattedContent() {
- let { content } = this;
-
- BIDI_CHARS.forEach((bidiChar) => {
- if (content.includes(bidiChar)) {
- content = content.replace(bidiChar, this.wrapBidiChar(bidiChar));
- }
- });
-
- return content;
- },
- },
- methods: {
- wrapBidiChar(bidiChar) {
- const span = document.createElement('span');
-
- setAttributes(span, {
- class: BIDI_CHARS_CLASS_LIST,
- title: BIDI_CHAR_TOOLTIP,
- 'data-testid': 'bidi-wrapper',
- });
-
- span.innerText = bidiChar;
-
- return span.outerHTML;
- },
- },
};
</script>
<template>
@@ -78,7 +48,7 @@ export default {
</div>
<pre
- class="gl-p-0! gl-w-full gl-overflow-visible! gl-border-none! code highlight gl-line-height-normal"
- ><code><span :id="`LC${number}`" v-safe-html="formattedContent" :lang="language" class="line" data-testid="content"></span></code></pre>
+ class="gl-p-0! gl-w-full gl-overflow-visible! gl-border-none! code highlight gl-line-height-0"
+ ><code><span :id="`LC${number}`" v-safe-html="content" :lang="language" class="line" data-testid="content"></span></code></pre>
</div>
</template>
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 30f57f506a6..a28460dd58e 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,13 +137,6 @@ 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 HLJS_COMMENT_SELECTOR = 'hljs-comment';
+export const BIDI_CHAR_TOOLTIP = 'Potentially unwanted character detected: Unicode BiDi Control';
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/index.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js
index 5d24a3d110b..d694adf7147 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 wrapChildNodes from './wrap_child_nodes';
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,7 +12,8 @@ import linkDependencies from './link_dependencies';
* @param {Object} hljs - the Highlight.js instance.
*/
export const registerPlugins = (hljs, fileType, rawContent) => {
- hljs.addPlugin({ [HLJS_ON_AFTER_HIGHLIGHT]: wrapComments });
+ hljs.addPlugin({ [HLJS_ON_AFTER_HIGHLIGHT]: wrapChildNodes });
+ hljs.addPlugin({ [HLJS_ON_AFTER_HIGHLIGHT]: wrapBidiChars });
hljs.addPlugin({
[HLJS_ON_AFTER_HIGHLIGHT]: (result) => linkDependencies(result, fileType, rawContent),
});
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 dbe6812cf16..49704421d6e 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) =>
+ `<a href="${escape(href)}" rel="nofollow noreferrer noopener">${escape(innerText)}</a>`;
export const generateHLJSOpenTag = (type, delimiter = '&quot;') =>
`<span class="hljs-${escape(type)}">${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 35de8fd13d6..46c9dc38300 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 = '&.*</span>';
@@ -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 3c6fc23c138..4bfd5ec2431 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 = '&quot;</span>';
@@ -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/plugins/wrap_bidi_chars.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_bidi_chars.js
new file mode 100644
index 00000000000..3b6cd96ef78
--- /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 `<span class="${BIDI_CHARS_CLASS_LIST}" title="${BIDI_CHAR_TOOLTIP}">${bidiChar}</span>`;
+}
+
+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/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_child_nodes.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_child_nodes.js
new file mode 100644
index 00000000000..e0ba4b730a7
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_child_nodes.js
@@ -0,0 +1,45 @@
+import { escape } from 'lodash';
+
+/**
+ * Highlight.js plugin for wrapping nodes with the correct selectors to ensure
+ * child-elements are highlighted correctly after we split up the result into chunks and lines.
+ *
+ * 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
+ */
+const newlineRegex = /\r?\n/;
+const generateClassName = (suffix) => (suffix ? `hljs-${escape(suffix)}` : '');
+const generateCloseTag = (includeClose) => (includeClose ? '</span>' : '');
+const generateHLJSTag = (kind, content = '', includeClose) =>
+ `<span class="${generateClassName(kind)}">${escape(content)}${generateCloseTag(includeClose)}`;
+
+const format = (node, kind = '') => {
+ let buffer = '';
+
+ if (typeof node === 'string') {
+ buffer += node
+ .split(newlineRegex)
+ .map((newline) => generateHLJSTag(kind, newline, true))
+ .join('\n');
+ } else if (node.kind) {
+ const { children } = node;
+ if (children.length && children.length === 1) {
+ buffer += format(children[0], node.kind);
+ } else {
+ buffer += generateHLJSTag(node.kind);
+ children.forEach((subChild) => {
+ buffer += format(subChild, node.kind);
+ });
+ buffer += `</span>`;
+ }
+ }
+
+ return buffer;
+};
+
+export default (result) => {
+ // NOTE: We're using the private Emitter API here as we expect the Emitter API to be publicly available soon (https://github.com/highlightjs/highlight.js/issues/3621)
+ // eslint-disable-next-line no-param-reassign, no-underscore-dangle
+ result.value = result._emitter.rootNode.children.reduce((val, node) => val + format(node), ''); // Highlight.js expects the result param to be mutated for plugins to work
+};
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_comments.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_comments.js
deleted file mode 100644
index 8b52df83fdf..00000000000
--- a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_comments.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import { HLJS_COMMENT_SELECTOR } from '../constants';
-
-const createWrapper = (content) => {
- const span = document.createElement('span');
- span.className = HLJS_COMMENT_SELECTOR;
-
- // eslint-disable-next-line no-unsanitized/property
- span.innerHTML = content;
- return span.outerHTML;
-};
-
-/**
- * Highlight.js plugin for wrapping multi-line comments in the `hljs-comment` class.
- * This ensures that multi-line comments are rendered correctly in the GitLab UI.
- *
- * 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
- */
-export default (result) => {
- if (!result.value.includes(HLJS_COMMENT_SELECTOR)) return;
-
- let wrapComment = false;
-
- // eslint-disable-next-line no-param-reassign
- result.value = result.value // Highlight.js expects the result param to be mutated for plugins to work
- .split('\n')
- .map((lineContent) => {
- const includesClosingTag = lineContent.includes('</span>');
- if (lineContent.includes(HLJS_COMMENT_SELECTOR) && !includesClosingTag) {
- wrapComment = true;
- return lineContent;
- }
- const line = wrapComment ? createWrapper(lineContent) : lineContent;
- if (includesClosingTag) {
- wrapComment = false;
- }
- return line;
- })
- .join('\n');
-};
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
index 9c6c12eac7d..536b2c8a281 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
@@ -53,7 +53,7 @@ export default {
},
computed: {
splitContent() {
- return this.content.split('\n');
+ return this.content.split(/\r?\n/);
},
lineNumbers() {
return this.splitContent.length;
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 00000000000..535e857d7a9
--- /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 00000000000..0da57f9e6fa
--- /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;
+};