diff options
Diffstat (limited to 'app/assets/javascripts/vue_shared/components/source_viewer')
3 files changed, 253 insertions, 0 deletions
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js new file mode 100644 index 00000000000..9efe0147c37 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js @@ -0,0 +1,111 @@ +// 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). +export const ROUGE_TO_HLJS_LANGUAGE_MAP = { + bsl: '1c', + actionscript: 'actionscript', + ada: 'ada', + apache: 'apache', + applescript: 'applescript', + armasm: 'armasm', + awk: 'awk', + c: 'c', + ceylon: 'ceylon', + clean: 'clean', + clojure: 'clojure', + cmake: 'cmake', + coffeescript: 'coffeescript', + coq: 'coq', + cpp: 'cpp', + crystal: 'crystal', + csharp: 'csharp', + css: 'css', + d: 'd', + dart: 'dart', + pascal: 'delphi', + diff: 'diff', + jinja: 'django', + docker: 'dockerfile', + batchfile: 'dos', + elixir: 'elixir', + elm: 'elm', + erb: 'erb', + erlang: 'erlang', + fortran: 'fortran', + fsharp: 'fsharp', + gherkin: 'gherkin', + glsl: 'glsl', + go: 'go', + gradle: 'gradle', + groovy: 'groovy', + haml: 'haml', + handlebars: 'handlebars', + haskell: 'haskell', + haxe: 'haxe', + http: 'http', + hylang: 'hy', + ini: 'ini', + isbl: 'isbl', + java: 'java', + javascript: 'javascript', + json: 'json', + julia: 'julia', + kotlin: 'kotlin', + lasso: 'lasso', + tex: 'latex', + common_lisp: 'lisp', + livescript: 'livescript', + llvm: 'llvm', + hlsl: 'lsl', + lua: 'lua', + make: 'makefile', + markdown: 'markdown', + mathematica: 'mathematica', + matlab: 'matlab', + moonscript: 'moonscript', + nginx: 'nginx', + nim: 'nim', + nix: 'nix', + objective_c: 'objectivec', + ocaml: 'ocaml', + perl: 'perl', + php: 'php', + plaintext: 'plaintext', + pony: 'pony', + powershell: 'powershell', + prolog: 'prolog', + properties: 'properties', + protobuf: 'protobuf', + puppet: 'puppet', + python: 'python', + q: 'q', + qml: 'qml', + r: 'r', + reasonml: 'reasonml', + ruby: 'ruby', + rust: 'rust', + sas: 'sas', + scala: 'scala', + scheme: 'scheme', + scss: 'scss', + shell: 'shell', + smalltalk: 'smalltalk', + sml: 'sml', + sqf: 'sqf', + sql: 'sql', + stan: 'stan', + stata: 'stata', + swift: 'swift', + tap: 'tap', + tcl: 'tcl', + twig: 'twig', + typescript: 'typescript', + vala: 'vala', + vb: 'vbnet', + verilog: 'verilog', + vhdl: 'vhdl', + viml: 'vim', + xml: 'xml', + xquery: 'xquery', + yaml: 'yaml', +}; 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 new file mode 100644 index 00000000000..5aae1812de3 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue @@ -0,0 +1,116 @@ +<script> +import { GlSafeHtmlDirective, GlLoadingIcon } from '@gitlab/ui'; +import LineNumbers from '~/vue_shared/components/line_numbers.vue'; +import { sanitize } from '~/lib/dompurify'; +import { ROUGE_TO_HLJS_LANGUAGE_MAP } from './constants'; +import { wrapLines } from './utils'; + +const LINE_SELECT_CLASS_NAME = 'hll'; + +export default { + components: { + LineNumbers, + GlLoadingIcon, + }, + directives: { + SafeHtml: GlSafeHtmlDirective, + }, + props: { + blob: { + type: Object, + required: true, + }, + }, + data() { + return { + languageDefinition: null, + content: this.blob.rawTextBlob, + language: ROUGE_TO_HLJS_LANGUAGE_MAP[this.blob.language], + hljs: null, + }; + }, + computed: { + lineNumbers() { + return this.content.split('\n').length; + }, + highlightedContent() { + let highlightedContent; + + if (this.hljs) { + if (!this.language) { + highlightedContent = this.hljs.highlightAuto(this.content).value; + } else if (this.languageDefinition) { + highlightedContent = this.hljs.highlight(this.content, { language: this.language }).value; + } + } + + return wrapLines(highlightedContent); + }, + }, + watch: { + highlightedContent() { + this.$nextTick(() => this.selectLine()); + }, + $route() { + this.selectLine(); + }, + }, + async mounted() { + this.hljs = await this.loadHighlightJS(); + + if (this.language) { + this.languageDefinition = await this.loadLanguage(); + } + }, + methods: { + loadHighlightJS() { + // If no language can be mapped to highlight.js we load all common languages else we load only the core (smallest footprint) + return !this.language ? import('highlight.js/lib/common') : import('highlight.js/lib/core'); + }, + async loadLanguage() { + let languageDefinition; + + try { + languageDefinition = await import(`highlight.js/lib/languages/${this.language}`); + this.hljs.registerLanguage(this.language, languageDefinition.default); + } catch (message) { + this.$emit('error', message); + } + + return languageDefinition; + }, + selectLine() { + const hash = sanitize(this.$route.hash); + const lineToSelect = hash && this.$el.querySelector(hash); + + if (!lineToSelect) { + return; + } + + if (this.$options.currentlySelectedLine) { + this.$options.currentlySelectedLine.classList.remove(LINE_SELECT_CLASS_NAME); + } + + lineToSelect.classList.add(LINE_SELECT_CLASS_NAME); + this.$options.currentlySelectedLine = lineToSelect; + lineToSelect.scrollIntoView({ behavior: 'smooth', block: 'center' }); + }, + }, + userColorScheme: window.gon.user_color_scheme, + currentlySelectedLine: null, +}; +</script> +<template> + <gl-loading-icon v-if="!highlightedContent" size="sm" class="gl-my-5" /> + <div + v-else + class="file-content code js-syntax-highlight blob-content gl-display-flex" + :class="$options.userColorScheme" + data-type="simple" + data-qa-selector="blob_viewer_file_content" + > + <line-numbers :lines="lineNumbers" /> + <pre class="code gl-pb-0!"><code v-safe-html="highlightedContent"></code> + </pre> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/utils.js b/app/assets/javascripts/vue_shared/components/source_viewer/utils.js new file mode 100644 index 00000000000..e64e564bf61 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/source_viewer/utils.js @@ -0,0 +1,26 @@ +export const wrapLines = (content) => { + return ( + content && + content + .split('\n') + .map((line, i) => { + let formattedLine; + const idAttribute = `id="LC${i + 1}"`; + + if (line.includes('<span class="hljs') && !line.includes('</span>')) { + /** + * In some cases highlight.js will wrap multiple lines in a span, in these cases we want to append the line number to the existing span + * + * example (before): <span class="hljs-code">```bash + * example (after): <span id="LC67" class="hljs-code">```bash + */ + formattedLine = line.replace(/(?=class="hljs)/, `${idAttribute} `); + } else { + formattedLine = `<span ${idAttribute} class="line">${line}</span>`; + } + + return formattedLine; + }) + .join('\n') + ); +}; |