Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-04-20 00:09:48 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-04-20 00:09:48 +0300
commit3007cf75a9bb55bcac18db7df56117677035d7b3 (patch)
tree67aa54b888aea5dd5ede2ebb63bae7f3147ee2ef /app/assets/javascripts/content_editor
parentcd40e11c5728c995c62e98091aff43b6e4aecabe (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/content_editor')
-rw-r--r--app/assets/javascripts/content_editor/components/code_block_bubble_menu.vue146
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor.vue3
-rw-r--r--app/assets/javascripts/content_editor/components/formatting_bubble_menu.vue13
-rw-r--r--app/assets/javascripts/content_editor/extensions/code_block_highlight.js37
-rw-r--r--app/assets/javascripts/content_editor/services/code_block_language_loader.js264
-rw-r--r--app/assets/javascripts/content_editor/services/create_content_editor.js3
-rw-r--r--app/assets/javascripts/content_editor/services/track_input_rules_and_shortcuts.js4
7 files changed, 439 insertions, 31 deletions
diff --git a/app/assets/javascripts/content_editor/components/code_block_bubble_menu.vue b/app/assets/javascripts/content_editor/components/code_block_bubble_menu.vue
new file mode 100644
index 00000000000..87f22a27856
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/code_block_bubble_menu.vue
@@ -0,0 +1,146 @@
+<script>
+import {
+ GlButton,
+ GlButtonGroup,
+ GlDropdown,
+ GlDropdownItem,
+ GlSearchBoxByType,
+ GlTooltipDirective as GlTooltip,
+} from '@gitlab/ui';
+import { BubbleMenu } from '@tiptap/vue-2';
+import codeBlockLanguageLoader from '../services/code_block_language_loader';
+import CodeBlockHighlight from '../extensions/code_block_highlight';
+import Diagram from '../extensions/diagram';
+import Frontmatter from '../extensions/frontmatter';
+import EditorStateObserver from './editor_state_observer.vue';
+
+const CODE_BLOCK_NODE_TYPES = [CodeBlockHighlight.name, Diagram.name, Frontmatter.name];
+
+export default {
+ components: {
+ BubbleMenu,
+ GlButton,
+ GlButtonGroup,
+ GlDropdown,
+ GlDropdownItem,
+ GlSearchBoxByType,
+ EditorStateObserver,
+ },
+ directives: {
+ GlTooltip,
+ },
+ inject: ['tiptapEditor'],
+ data() {
+ return {
+ selectedLanguage: {},
+ filterTerm: '',
+ filteredLanguages: [],
+ };
+ },
+ watch: {
+ filterTerm: {
+ handler(val) {
+ this.filteredLanguages = codeBlockLanguageLoader.filterLanguages(val);
+ },
+ immediate: true,
+ },
+ },
+ methods: {
+ shouldShow: ({ editor }) => {
+ return CODE_BLOCK_NODE_TYPES.some((type) => editor.isActive(type));
+ },
+
+ getSelectedLanguage() {
+ const { language } = this.tiptapEditor.getAttributes(this.getCodeBlockType());
+
+ this.selectedLanguage = codeBlockLanguageLoader.findLanguageBySyntax(language);
+ },
+
+ async setSelectedLanguage(language) {
+ this.selectedLanguage = language;
+
+ await codeBlockLanguageLoader.loadLanguages([language.syntax]);
+
+ this.tiptapEditor.commands.setCodeBlock({ language: this.selectedLanguage.syntax });
+ },
+
+ tippyOnBeforeUpdate(tippy, props) {
+ if (props.getReferenceClientRect) {
+ // eslint-disable-next-line no-param-reassign
+ props.getReferenceClientRect = () => {
+ const { view } = this.tiptapEditor;
+ const { from } = this.tiptapEditor.state.selection;
+
+ for (let { node } = view.domAtPos(from); node; node = node.parentElement) {
+ if (node.nodeName?.toLowerCase() === 'pre') {
+ return node.getBoundingClientRect();
+ }
+ }
+
+ return new DOMRect(-1000, -1000, 0, 0);
+ };
+ }
+ },
+
+ deleteCodeBlock() {
+ this.tiptapEditor.chain().focus().deleteNode(this.getCodeBlockType()).run();
+ },
+
+ getCodeBlockType() {
+ return (
+ CODE_BLOCK_NODE_TYPES.find((type) => this.tiptapEditor.isActive(type)) ||
+ CodeBlockHighlight.name
+ );
+ },
+ },
+};
+</script>
+<template>
+ <bubble-menu
+ data-testid="code-block-bubble-menu"
+ class="gl-shadow gl-rounded-base"
+ :editor="tiptapEditor"
+ plugin-key="bubbleMenuCodeBlock"
+ :should-show="shouldShow"
+ :tippy-options="{ onBeforeUpdate: tippyOnBeforeUpdate }"
+ >
+ <editor-state-observer @transaction="getSelectedLanguage">
+ <gl-button-group>
+ <gl-dropdown contenteditable="false" boundary="viewport" :text="selectedLanguage.label">
+ <template #header>
+ <gl-search-box-by-type
+ v-model="filterTerm"
+ :clear-button-title="__('Clear')"
+ :placeholder="__('Search')"
+ />
+ </template>
+
+ <template #highlighted-items>
+ <gl-dropdown-item :key="selectedLanguage.syntax" is-check-item :is-checked="true">
+ {{ selectedLanguage.label }}
+ </gl-dropdown-item>
+ </template>
+
+ <gl-dropdown-item
+ v-for="language in filteredLanguages"
+ v-show="selectedLanguage.syntax !== language.syntax"
+ :key="language.syntax"
+ @click="setSelectedLanguage(language)"
+ >
+ {{ language.label }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ <gl-button
+ v-gl-tooltip
+ variant="default"
+ category="primary"
+ size="medium"
+ :aria-label="__('Delete code block')"
+ :title="__('Delete code block')"
+ icon="remove"
+ @click="deleteCodeBlock"
+ />
+ </gl-button-group>
+ </editor-state-observer>
+ </bubble-menu>
+</template>
diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue
index a942c9f1149..5b3f4f4ddf2 100644
--- a/app/assets/javascripts/content_editor/components/content_editor.vue
+++ b/app/assets/javascripts/content_editor/components/content_editor.vue
@@ -5,6 +5,7 @@ import ContentEditorAlert from './content_editor_alert.vue';
import ContentEditorProvider from './content_editor_provider.vue';
import EditorStateObserver from './editor_state_observer.vue';
import FormattingBubbleMenu from './formatting_bubble_menu.vue';
+import CodeBlockBubbleMenu from './code_block_bubble_menu.vue';
import TopToolbar from './top_toolbar.vue';
import LoadingIndicator from './loading_indicator.vue';
@@ -16,6 +17,7 @@ export default {
TiptapEditorContent,
TopToolbar,
FormattingBubbleMenu,
+ CodeBlockBubbleMenu,
EditorStateObserver,
},
props: {
@@ -89,6 +91,7 @@ export default {
<top-toolbar ref="toolbar" class="gl-mb-4" />
<div class="gl-relative">
<formatting-bubble-menu />
+ <code-block-bubble-menu />
<tiptap-editor-content class="md" :editor="contentEditor.tiptapEditor" />
<loading-indicator />
</div>
diff --git a/app/assets/javascripts/content_editor/components/formatting_bubble_menu.vue b/app/assets/javascripts/content_editor/components/formatting_bubble_menu.vue
index 14a553ff30b..103079534bc 100644
--- a/app/assets/javascripts/content_editor/components/formatting_bubble_menu.vue
+++ b/app/assets/javascripts/content_editor/components/formatting_bubble_menu.vue
@@ -3,6 +3,10 @@ import { GlButtonGroup } from '@gitlab/ui';
import { BubbleMenu } from '@tiptap/vue-2';
import { BUBBLE_MENU_TRACKING_ACTION } from '../constants';
import trackUIControl from '../services/track_ui_control';
+import Code from '../extensions/code';
+import CodeBlockHighlight from '../extensions/code_block_highlight';
+import Diagram from '../extensions/diagram';
+import Frontmatter from '../extensions/frontmatter';
import ToolbarButton from './toolbar_button.vue';
export default {
@@ -16,6 +20,14 @@ export default {
trackToolbarControlExecution({ contentType, value }) {
trackUIControl({ action: BUBBLE_MENU_TRACKING_ACTION, property: contentType, value });
},
+
+ shouldShow: ({ editor, from, to }) => {
+ if (from === to) return false;
+
+ const exclude = [Code.name, CodeBlockHighlight.name, Diagram.name, Frontmatter.name];
+
+ return !exclude.some((type) => editor.isActive(type));
+ },
},
};
</script>
@@ -24,6 +36,7 @@ export default {
data-testid="formatting-bubble-menu"
class="gl-shadow gl-rounded-base"
:editor="tiptapEditor"
+ :should-show="shouldShow"
>
<gl-button-group>
<toolbar-button
diff --git a/app/assets/javascripts/content_editor/extensions/code_block_highlight.js b/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
index 74f620b57b6..61f379fc0a2 100644
--- a/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
+++ b/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
@@ -1,30 +1,19 @@
import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight';
import { textblockTypeInputRule } from '@tiptap/core';
-import { isFunction } from 'lodash';
+import codeBlockLanguageLoader from '../services/code_block_language_loader';
const extractLanguage = (element) => element.getAttribute('lang');
-const backtickInputRegex = /^```([a-z]+)?[\s\n]$/;
-const tildeInputRegex = /^~~~([a-z]+)?[\s\n]$/;
-
-const loadLanguageFromInputRule = (languageLoader) => (match) => {
- const language = match[1];
-
- if (isFunction(languageLoader?.loadLanguages)) {
- languageLoader.loadLanguages([language]);
- }
-
- return {
- language,
- };
-};
+export const backtickInputRegex = /^```([a-z]+)?[\s\n]$/;
+export const tildeInputRegex = /^~~~([a-z]+)?[\s\n]$/;
export default CodeBlockLowlight.extend({
isolating: true,
+ exitOnArrowDown: false,
addOptions() {
return {
...this.parent?.(),
- languageLoader: {},
+ languageLoader: codeBlockLanguageLoader,
};
},
@@ -42,26 +31,36 @@ export default CodeBlockLowlight.extend({
},
addInputRules() {
const { languageLoader } = this.options;
+ const getAttributes = (match) => languageLoader?.loadLanguageFromInputRule(match) || {};
return [
textblockTypeInputRule({
find: backtickInputRegex,
type: this.type,
- getAttributes: loadLanguageFromInputRule(languageLoader),
+ getAttributes,
}),
textblockTypeInputRule({
find: tildeInputRegex,
type: this.type,
- getAttributes: loadLanguageFromInputRule(languageLoader),
+ getAttributes,
}),
];
},
+ parseHTML() {
+ return [
+ ...(this.parent?.() || []),
+ {
+ tag: 'div.markdown-code-block',
+ skip: true,
+ },
+ ];
+ },
renderHTML({ HTMLAttributes }) {
return [
'pre',
{
...HTMLAttributes,
- class: `content-editor-code-block ${HTMLAttributes.class}`,
+ class: `content-editor-code-block ${gon.user_color_scheme} ${HTMLAttributes.class}`,
},
['code', {}, 0],
];
diff --git a/app/assets/javascripts/content_editor/services/code_block_language_loader.js b/app/assets/javascripts/content_editor/services/code_block_language_loader.js
index 3c12cf614a5..081400cfd9a 100644
--- a/app/assets/javascripts/content_editor/services/code_block_language_loader.js
+++ b/app/assets/javascripts/content_editor/services/code_block_language_loader.js
@@ -1,11 +1,249 @@
-export default class CodeBlockLanguageLoader {
- constructor(lowlight) {
- this.lowlight = lowlight;
- }
+import { lowlight } from 'lowlight/lib/core';
+import { __, sprintf } from '~/locale';
+
+/* eslint-disable @gitlab/require-i18n-strings */
+// List of languages referenced from https://github.com/wooorm/lowlight#data
+const CODE_BLOCK_LANGUAGES = [
+ { syntax: '1c', label: '1C:Enterprise' },
+ { syntax: 'abnf', label: 'Augmented Backus-Naur Form' },
+ { syntax: 'accesslog', label: 'Apache Access Log' },
+ { syntax: 'actionscript', variants: 'as', label: 'ActionScript' },
+ { syntax: 'ada', label: 'Ada' },
+ { syntax: 'angelscript', variants: 'asc', label: 'AngelScript' },
+ { syntax: 'apache', variants: 'apacheconf', label: 'Apache config' },
+ { syntax: 'applescript', variants: 'osascript', label: 'AppleScript' },
+ { syntax: 'arcade', label: 'ArcGIS Arcade' },
+ { syntax: 'arduino', variants: 'ino', label: 'Arduino' },
+ { syntax: 'armasm', variants: 'arm', label: 'ARM Assembly' },
+ { syntax: 'asciidoc', variants: 'adoc', label: 'AsciiDoc' },
+ { syntax: 'aspectj', label: 'AspectJ' },
+ { syntax: 'autohotkey', variants: 'ahk', label: 'AutoHotkey' },
+ { syntax: 'autoit', label: 'AutoIt' },
+ { syntax: 'avrasm', label: 'AVR Assembly' },
+ { syntax: 'awk', label: 'Awk' },
+ { syntax: 'axapta', variants: 'x++', label: 'X++' },
+ { syntax: 'bash', variants: 'sh', label: 'Bash' },
+ { syntax: 'basic', label: 'BASIC' },
+ { syntax: 'bnf', label: 'Backus-Naur Form' },
+ { syntax: 'brainfuck', variants: 'bf', label: 'Brainfuck' },
+ { syntax: 'c', variants: 'h', label: 'C' },
+ { syntax: 'cal', label: 'C/AL' },
+ { syntax: 'capnproto', variants: 'capnp', label: "Cap'n Proto" },
+ { syntax: 'ceylon', label: 'Ceylon' },
+ { syntax: 'clean', variants: 'icl, dcl', label: 'Clean' },
+ { syntax: 'clojure', variants: 'clj, edn', label: 'Clojure' },
+ { syntax: 'clojure-repl', label: 'Clojure REPL' },
+ { syntax: 'cmake', variants: 'cmake.in', label: 'CMake' },
+ { syntax: 'coffeescript', variants: 'coffee, cson, iced', label: 'CoffeeScript' },
+ { syntax: 'coq', label: 'Coq' },
+ { syntax: 'cos', variants: 'cls', label: 'Caché Object Script' },
+ { syntax: 'cpp', variants: 'cc, c++, h++, hpp, hh, hxx, cxx', label: 'C++' },
+ { syntax: 'crmsh', variants: 'crm, pcmk', label: 'crmsh' },
+ { syntax: 'crystal', variants: 'cr', label: 'Crystal' },
+ { syntax: 'csharp', variants: 'cs, c#', label: 'C#' },
+ { syntax: 'csp', label: 'CSP' },
+ { syntax: 'css', label: 'CSS' },
+ { syntax: 'd', label: 'D' },
+ { syntax: 'dart', label: 'Dart' },
+ { syntax: 'delphi', variants: 'dpr, dfm, pas, pascal', label: 'Delphi' },
+ { syntax: 'diff', variants: 'patch', label: 'Diff' },
+ { syntax: 'django', variants: 'jinja', label: 'Django' },
+ { syntax: 'dns', variants: 'bind, zone', label: 'DNS Zone' },
+ { syntax: 'dockerfile', variants: 'docker', label: 'Dockerfile' },
+ { syntax: 'dos', variants: 'bat, cmd', label: 'Batch file (DOS)' },
+ { syntax: 'dsconfig', label: 'DSConfig' },
+ { syntax: 'dts', label: 'Device Tree' },
+ { syntax: 'dust', variants: 'dst', label: 'Dust' },
+ { syntax: 'ebnf', label: 'Extended Backus-Naur Form' },
+ { syntax: 'elixir', variants: 'ex, exs', label: 'Elixir' },
+ { syntax: 'elm', label: 'Elm' },
+ { syntax: 'erb', label: 'ERB' },
+ { syntax: 'erlang', variants: 'erl', label: 'Erlang' },
+ { syntax: 'erlang-repl', label: 'Erlang REPL' },
+ { syntax: 'excel', variants: 'xlsx, xls', label: 'Excel formulae' },
+ { syntax: 'fix', label: 'FIX' },
+ { syntax: 'flix', label: 'Flix' },
+ { syntax: 'fortran', variants: 'f90, f95', label: 'Fortran' },
+ { syntax: 'fsharp', variants: 'fs, f#', label: 'F#' },
+ { syntax: 'gams', variants: 'gms', label: 'GAMS' },
+ { syntax: 'gauss', variants: 'gss', label: 'GAUSS' },
+ { syntax: 'gcode', variants: 'nc', label: 'G-code (ISO 6983)' },
+ { syntax: 'gherkin', variants: 'feature', label: 'Gherkin' },
+ { syntax: 'glsl', label: 'GLSL' },
+ { syntax: 'gml', label: 'GML' },
+ { syntax: 'go', variants: 'golang', label: 'Go' },
+ { syntax: 'golo', label: 'Golo' },
+ { syntax: 'gradle', label: 'Gradle' },
+ { syntax: 'graphql', variants: 'gql', label: 'GraphQL' },
+ { syntax: 'groovy', label: 'Groovy' },
+ { syntax: 'haml', label: 'HAML' },
+ {
+ syntax: 'handlebars',
+ variants: 'hbs, html.hbs, html.handlebars, htmlbars',
+ label: 'Handlebars',
+ },
+ { syntax: 'haskell', variants: 'hs', label: 'Haskell' },
+ { syntax: 'haxe', variants: 'hx', label: 'Haxe' },
+ { syntax: 'hsp', label: 'HSP' },
+ { syntax: 'http', variants: 'https', label: 'HTTP' },
+ { syntax: 'hy', variants: 'hylang', label: 'Hy' },
+ { syntax: 'inform7', variants: 'i7', label: 'Inform 7' },
+ { syntax: 'ini', variants: 'toml', label: 'TOML, also INI' },
+ { syntax: 'irpf90', label: 'IRPF90' },
+ { syntax: 'isbl', label: 'ISBL' },
+ { syntax: 'java', variants: 'jsp', label: 'Java' },
+ { syntax: 'javascript', variants: 'js, jsx, mjs, cjs', label: 'Javascript' },
+ { syntax: 'jboss-cli', variants: 'wildfly-cli', label: 'JBoss CLI' },
+ { syntax: 'json', label: 'JSON' },
+ { syntax: 'julia', label: 'Julia' },
+ { syntax: 'julia-repl', variants: 'jldoctest', label: 'Julia REPL' },
+ { syntax: 'kotlin', variants: 'kt, kts', label: 'Kotlin' },
+ { syntax: 'lasso', variants: 'ls, lassoscript', label: 'Lasso' },
+ { syntax: 'latex', variants: 'tex', label: 'LaTeX' },
+ { syntax: 'ldif', label: 'LDIF' },
+ { syntax: 'leaf', label: 'Leaf' },
+ { syntax: 'less', label: 'Less' },
+ { syntax: 'lisp', label: 'Lisp' },
+ { syntax: 'livecodeserver', label: 'LiveCode' },
+ { syntax: 'livescript', variants: 'ls', label: 'LiveScript' },
+ { syntax: 'llvm', label: 'LLVM IR' },
+ { syntax: 'lsl', label: 'LSL (Linden Scripting Language)' },
+ { syntax: 'lua', label: 'Lua' },
+ { syntax: 'makefile', variants: 'mk, mak, make', label: 'Makefile' },
+ { syntax: 'markdown', variants: 'md, mkdown, mkd', label: 'Markdown' },
+ { syntax: 'mathematica', variants: 'mma, wl', label: 'Mathematica' },
+ { syntax: 'matlab', label: 'Matlab' },
+ { syntax: 'maxima', label: 'Maxima' },
+ { syntax: 'mel', label: 'MEL' },
+ { syntax: 'mercury', variants: 'm, moo', label: 'Mercury' },
+ { syntax: 'mipsasm', variants: 'mips', label: 'MIPS Assembly' },
+ { syntax: 'mizar', label: 'Mizar' },
+ { syntax: 'mojolicious', label: 'Mojolicious' },
+ { syntax: 'monkey', label: 'Monkey' },
+ { syntax: 'moonscript', variants: 'moon', label: 'MoonScript' },
+ { syntax: 'n1ql', label: 'N1QL' },
+ { syntax: 'nestedtext', variants: 'nt', label: 'Nested Text' },
+ { syntax: 'nginx', variants: 'nginxconf', label: 'Nginx config' },
+ { syntax: 'nim', label: 'Nim' },
+ { syntax: 'nix', variants: 'nixos', label: 'Nix' },
+ { syntax: 'node-repl', label: 'Node REPL' },
+ { syntax: 'nsis', label: 'NSIS' },
+ {
+ syntax: 'objectivec',
+ variants: 'mm, objc, obj-c, obj-c++, objective-c++',
+ label: 'Objective-C',
+ },
+ { syntax: 'ocaml', variants: 'ml', label: 'OCaml' },
+ { syntax: 'openscad', variants: 'scad', label: 'OpenSCAD' },
+ { syntax: 'oxygene', label: 'Oxygene' },
+ { syntax: 'parser3', label: 'Parser3' },
+ { syntax: 'perl', variants: 'pl, pm', label: 'Perl' },
+ { syntax: 'pf', variants: 'pf.conf', label: 'Packet Filter config' },
+ { syntax: 'pgsql', variants: 'postgres, postgresql', label: 'PostgreSQL' },
+ { syntax: 'php', label: 'PHP' },
+ { syntax: 'php-template', label: 'PHP template' },
+ { syntax: 'plaintext', variants: 'text, txt', label: 'Plain text' },
+ { syntax: 'pony', label: 'Pony' },
+ { syntax: 'powershell', variants: 'pwsh, ps, ps1', label: 'PowerShell' },
+ { syntax: 'processing', variants: 'pde', label: 'Processing' },
+ { syntax: 'profile', label: 'Python profiler' },
+ { syntax: 'prolog', label: 'Prolog' },
+ { syntax: 'properties', label: '.properties' },
+ { syntax: 'protobuf', label: 'Protocol Buffers' },
+ { syntax: 'puppet', variants: 'pp', label: 'Puppet' },
+ { syntax: 'purebasic', variants: 'pb, pbi', label: 'PureBASIC' },
+ { syntax: 'python', variants: 'py, gyp, ipython', label: 'Python' },
+ { syntax: 'python-repl', variants: 'pycon', label: 'Python REPL' },
+ { syntax: 'q', variants: 'k, kdb', label: 'Q' },
+ { syntax: 'qml', variants: 'qt', label: 'QML' },
+ { syntax: 'r', label: 'R' },
+ { syntax: 'reasonml', variants: 're', label: 'ReasonML' },
+ { syntax: 'rib', label: 'RenderMan RIB' },
+ { syntax: 'roboconf', variants: 'graph, instances', label: 'Roboconf' },
+ { syntax: 'routeros', variants: 'mikrotik', label: 'Microtik RouterOS script' },
+ { syntax: 'rsl', label: 'RenderMan RSL' },
+ { syntax: 'ruby', variants: 'rb, gemspec, podspec, thor, irb', label: 'Ruby' },
+ { syntax: 'ruleslanguage', label: 'Oracle Rules Language' },
+ { syntax: 'rust', variants: 'rs', label: 'Rust' },
+ { syntax: 'sas', label: 'SAS' },
+ { syntax: 'scala', label: 'Scala' },
+ { syntax: 'scheme', label: 'Scheme' },
+ { syntax: 'scilab', variants: 'sci', label: 'Scilab' },
+ { syntax: 'scss', label: 'SCSS' },
+ { syntax: 'shell', variants: 'console, shellsession', label: 'Shell Session' },
+ { syntax: 'smali', label: 'Smali' },
+ { syntax: 'smalltalk', variants: 'st', label: 'Smalltalk' },
+ { syntax: 'sml', variants: 'ml', label: 'SML (Standard ML)' },
+ { syntax: 'sqf', label: 'SQF' },
+ { syntax: 'sql', label: 'SQL' },
+ { syntax: 'stan', variants: 'stanfuncs', label: 'Stan' },
+ { syntax: 'stata', variants: 'do, ado', label: 'Stata' },
+ { syntax: 'step21', variants: 'p21, step, stp', label: 'STEP Part 21' },
+ { syntax: 'stylus', variants: 'styl', label: 'Stylus' },
+ { syntax: 'subunit', label: 'SubUnit' },
+ { syntax: 'swift', label: 'Swift' },
+ { syntax: 'taggerscript', label: 'Tagger Script' },
+ { syntax: 'tap', label: 'Test Anything Protocol' },
+ { syntax: 'tcl', variants: 'tk', label: 'Tcl' },
+ { syntax: 'thrift', label: 'Thrift' },
+ { syntax: 'tp', label: 'TP' },
+ { syntax: 'twig', variants: 'craftcms', label: 'Twig' },
+ { syntax: 'typescript', variants: 'ts, tsx', label: 'TypeScript' },
+ { syntax: 'vala', label: 'Vala' },
+ { syntax: 'vbnet', variants: 'vb', label: 'Visual Basic .NET' },
+ { syntax: 'vbscript', variants: 'vbs', label: 'VBScript' },
+ { syntax: 'vbscript-html', label: 'VBScript in HTML' },
+ { syntax: 'verilog', variants: 'v, sv, svh', label: 'Verilog' },
+ { syntax: 'vhdl', label: 'VHDL' },
+ { syntax: 'vim', label: 'Vim Script' },
+ { syntax: 'wasm', label: 'WebAssembly' },
+ { syntax: 'wren', label: 'Wren' },
+ { syntax: 'x86asm', label: 'Intel x86 Assembly' },
+ { syntax: 'xl', variants: 'tao', label: 'XL' },
+ {
+ syntax: 'xml',
+ variants: 'html, xhtml, rss, atom, xjb, xsd, xsl, plist, wsf, svg',
+ label: 'HTML, XML',
+ },
+ { syntax: 'xquery', variants: 'xpath, xq', label: 'XQuery' },
+ { syntax: 'yaml', variants: 'yml', label: 'YAML' },
+ { syntax: 'zephir', variants: 'zep', label: 'Zephir' },
+];
+/* eslint-enable @gitlab/require-i18n-strings */
+
+const codeBlockLanguageLoader = {
+ lowlight,
+
+ allLanguages: CODE_BLOCK_LANGUAGES,
+
+ findLanguageBySyntax(value) {
+ const lowercaseValue = value?.toLowerCase() || 'plaintext';
+ return (
+ this.allLanguages.find(
+ ({ syntax, variants }) =>
+ syntax === lowercaseValue || variants?.toLowerCase().split(', ').includes(lowercaseValue),
+ ) || {
+ syntax: lowercaseValue,
+ label: sprintf(__(`Custom (%{language})`), { language: lowercaseValue }),
+ }
+ );
+ },
+
+ filterLanguages(value) {
+ if (!value) return this.allLanguages;
+
+ const lowercaseValue = value?.toLowerCase() || '';
+ return this.allLanguages.filter(
+ ({ syntax, label, variants }) =>
+ syntax.toLowerCase().includes(lowercaseValue) ||
+ label.toLowerCase().includes(lowercaseValue) ||
+ variants?.toLowerCase().includes(lowercaseValue),
+ );
+ },
isLanguageLoaded(language) {
return this.lowlight.registered(language);
- }
+ },
loadLanguagesFromDOM(domTree) {
const languages = [];
@@ -15,7 +253,15 @@ export default class CodeBlockLanguageLoader {
});
return this.loadLanguages(languages);
- }
+ },
+
+ loadLanguageFromInputRule(match) {
+ const { syntax } = this.findLanguageBySyntax(match[1]);
+
+ this.loadLanguages([syntax]);
+
+ return { language: syntax };
+ },
loadLanguages(languageList = []) {
const loaders = languageList
@@ -31,5 +277,7 @@ export default class CodeBlockLanguageLoader {
});
return Promise.all(loaders);
- }
-}
+ },
+};
+
+export default codeBlockLanguageLoader;
diff --git a/app/assets/javascripts/content_editor/services/create_content_editor.js b/app/assets/javascripts/content_editor/services/create_content_editor.js
index 7ed62ee17fb..af19a0ab0e4 100644
--- a/app/assets/javascripts/content_editor/services/create_content_editor.js
+++ b/app/assets/javascripts/content_editor/services/create_content_editor.js
@@ -60,7 +60,7 @@ import { ContentEditor } from './content_editor';
import createMarkdownSerializer from './markdown_serializer';
import createMarkdownDeserializer from './markdown_deserializer';
import trackInputRulesAndShortcuts from './track_input_rules_and_shortcuts';
-import CodeBlockLanguageLoader from './code_block_language_loader';
+import languageLoader from './code_block_language_loader';
const createTiptapEditor = ({ extensions = [], ...options } = {}) =>
new Editor({
@@ -86,7 +86,6 @@ export const createContentEditor = ({
const eventHub = eventHubFactory();
- const languageLoader = new CodeBlockLanguageLoader(lowlight);
const builtInContentEditorExtensions = [
Attachment.configure({ uploadsPath, renderMarkdown, eventHub }),
Audio,
diff --git a/app/assets/javascripts/content_editor/services/track_input_rules_and_shortcuts.js b/app/assets/javascripts/content_editor/services/track_input_rules_and_shortcuts.js
index eb1e4885ba6..b844b414343 100644
--- a/app/assets/javascripts/content_editor/services/track_input_rules_and_shortcuts.js
+++ b/app/assets/javascripts/content_editor/services/track_input_rules_and_shortcuts.js
@@ -8,12 +8,12 @@ import {
INPUT_RULE_TRACKING_ACTION,
} from '../constants';
-const trackKeyboardShortcut = (contentType, commandFn, shortcut) => () => {
+const trackKeyboardShortcut = (contentType, commandFn, shortcut) => (...args) => {
Tracking.event(undefined, KEYBOARD_SHORTCUT_TRACKING_ACTION, {
label: CONTENT_EDITOR_TRACKING_LABEL,
property: `${contentType}.${shortcut}`,
});
- return commandFn();
+ return commandFn(...args);
};
const trackInputRule = (contentType, inputRule) => {