diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-03-23 15:07:27 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-03-23 15:07:27 +0300 |
commit | 3e68d3848770b492d314f8e2967c37f7fdd5d143 (patch) | |
tree | 01bd69a759c55ddf4ea1e5549a253cb0fd564854 /app/assets/javascripts/content_editor | |
parent | 52192e0f19ca790dc9f44bc45730434100f83d90 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/content_editor')
5 files changed, 96 insertions, 10 deletions
diff --git a/app/assets/javascripts/content_editor/components/loading_indicator.vue b/app/assets/javascripts/content_editor/components/loading_indicator.vue index 5b9383d6e11..620324adb06 100644 --- a/app/assets/javascripts/content_editor/components/loading_indicator.vue +++ b/app/assets/javascripts/content_editor/components/loading_indicator.vue @@ -30,6 +30,7 @@ export default { > <div v-if="isLoading" + data-testid="content-editor-loading-indicator" class="gl-w-full gl-display-flex gl-justify-content-center gl-align-items-center gl-absolute gl-top-0 gl-bottom-0" > <div class="gl-bg-white gl-absolute gl-w-full gl-h-full gl-opacity-3"></div> 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 204ac07d401..74f620b57b6 100644 --- a/app/assets/javascripts/content_editor/extensions/code_block_highlight.js +++ b/app/assets/javascripts/content_editor/extensions/code_block_highlight.js @@ -1,11 +1,33 @@ import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight'; -import { lowlight } from 'lowlight/lib/all'; +import { textblockTypeInputRule } from '@tiptap/core'; +import { isFunction } from 'lodash'; 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 default CodeBlockLowlight.extend({ isolating: true, + addOptions() { + return { + ...this.parent?.(), + languageLoader: {}, + }; + }, + addAttributes() { return { language: { @@ -18,6 +40,22 @@ export default CodeBlockLowlight.extend({ }, }; }, + addInputRules() { + const { languageLoader } = this.options; + + return [ + textblockTypeInputRule({ + find: backtickInputRegex, + type: this.type, + getAttributes: loadLanguageFromInputRule(languageLoader), + }), + textblockTypeInputRule({ + find: tildeInputRegex, + type: this.type, + getAttributes: loadLanguageFromInputRule(languageLoader), + }), + ]; + }, renderHTML({ HTMLAttributes }) { return [ 'pre', @@ -28,6 +66,4 @@ export default CodeBlockLowlight.extend({ ['code', {}, 0], ]; }, -}).configure({ - lowlight, }); 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 new file mode 100644 index 00000000000..3c12cf614a5 --- /dev/null +++ b/app/assets/javascripts/content_editor/services/code_block_language_loader.js @@ -0,0 +1,35 @@ +export default class CodeBlockLanguageLoader { + constructor(lowlight) { + this.lowlight = lowlight; + } + + isLanguageLoaded(language) { + return this.lowlight.registered(language); + } + + loadLanguagesFromDOM(domTree) { + const languages = []; + + domTree.querySelectorAll('pre').forEach((preElement) => { + languages.push(preElement.getAttribute('lang')); + }); + + return this.loadLanguages(languages); + } + + loadLanguages(languageList = []) { + const loaders = languageList + .filter((languageName) => !this.isLanguageLoaded(languageName)) + .map((languageName) => { + return import( + /* webpackChunkName: 'highlight.language.js' */ `highlight.js/lib/languages/${languageName}` + ) + .then(({ default: language }) => { + this.lowlight.registerLanguage(languageName, language); + }) + .catch(() => false); + }); + + return Promise.all(loaders); + } +} diff --git a/app/assets/javascripts/content_editor/services/content_editor.js b/app/assets/javascripts/content_editor/services/content_editor.js index c5638da2daf..56badf965ee 100644 --- a/app/assets/javascripts/content_editor/services/content_editor.js +++ b/app/assets/javascripts/content_editor/services/content_editor.js @@ -3,11 +3,12 @@ import { LOADING_CONTENT_EVENT, LOADING_SUCCESS_EVENT, LOADING_ERROR_EVENT } fro /* eslint-disable no-underscore-dangle */ export class ContentEditor { - constructor({ tiptapEditor, serializer, deserializer, eventHub }) { + constructor({ tiptapEditor, serializer, deserializer, eventHub, languageLoader }) { this._tiptapEditor = tiptapEditor; this._serializer = serializer; this._deserializer = deserializer; this._eventHub = eventHub; + this._languageLoader = languageLoader; } get tiptapEditor() { @@ -34,23 +35,33 @@ export class ContentEditor { } async setSerializedContent(serializedContent) { - const { _tiptapEditor: editor, _deserializer: deserializer, _eventHub: eventHub } = this; + const { + _tiptapEditor: editor, + _deserializer: deserializer, + _eventHub: eventHub, + _languageLoader: languageLoader, + } = this; const { doc, tr } = editor.state; const selection = TextSelection.create(doc, 0, doc.content.size); try { eventHub.$emit(LOADING_CONTENT_EVENT); - const { document } = await deserializer.deserialize({ + const result = await deserializer.deserialize({ schema: editor.schema, content: serializedContent, }); - if (document) { + if (Object.keys(result).length !== 0) { + const { document, dom } = result; + + await languageLoader.loadLanguagesFromDOM(dom); + tr.setSelection(selection) .replaceSelectionWith(document, false) .setMeta('preventUpdate', true); editor.view.dispatch(tr); } + eventHub.$emit(LOADING_SUCCESS_EVENT); } catch (e) { eventHub.$emit(LOADING_ERROR_EVENT, e); 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 d9d39a387d0..5b637eee176 100644 --- a/app/assets/javascripts/content_editor/services/create_content_editor.js +++ b/app/assets/javascripts/content_editor/services/create_content_editor.js @@ -1,5 +1,6 @@ import { Editor } from '@tiptap/vue-2'; import { isFunction } from 'lodash'; +import { lowlight } from 'lowlight/lib/core'; import eventHubFactory from '~/helpers/event_hub_factory'; import { PROVIDE_SERIALIZER_OR_RENDERER_ERROR } from '../constants'; import Attachment from '../extensions/attachment'; @@ -58,6 +59,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'; const createTiptapEditor = ({ extensions = [], ...options } = {}) => new Editor({ @@ -83,6 +85,7 @@ export const createContentEditor = ({ const eventHub = eventHubFactory(); + const languageLoader = new CodeBlockLanguageLoader(lowlight); const builtInContentEditorExtensions = [ Attachment.configure({ uploadsPath, renderMarkdown, eventHub }), Audio, @@ -91,7 +94,7 @@ export const createContentEditor = ({ BulletList, Code, ColorChip, - CodeBlockHighlight, + CodeBlockHighlight.configure({ lowlight, languageLoader }), DescriptionItem, DescriptionList, Details, @@ -105,7 +108,7 @@ export const createContentEditor = ({ FootnoteDefinition, FootnoteReference, FootnotesSection, - Frontmatter, + Frontmatter.configure({ lowlight }), Gapcursor, HardBreak, Heading, @@ -144,5 +147,5 @@ export const createContentEditor = ({ const serializer = createMarkdownSerializer({ serializerConfig }); const deserializer = createMarkdownDeserializer({ render: renderMarkdown }); - return new ContentEditor({ tiptapEditor, serializer, eventHub, deserializer }); + return new ContentEditor({ tiptapEditor, serializer, eventHub, deserializer, languageLoader }); }; |