diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-04-20 00:09:48 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-04-20 00:09:48 +0300 |
commit | 3007cf75a9bb55bcac18db7df56117677035d7b3 (patch) | |
tree | 67aa54b888aea5dd5ede2ebb63bae7f3147ee2ef /app/assets/javascripts/content_editor | |
parent | cd40e11c5728c995c62e98091aff43b6e4aecabe (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/content_editor')
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) => { |