diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-10-20 11:43:02 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-10-20 11:43:02 +0300 |
commit | d9ab72d6080f594d0b3cae15f14b3ef2c6c638cb (patch) | |
tree | 2341ef426af70ad1e289c38036737e04b0aa5007 /app/assets/javascripts/content_editor/extensions | |
parent | d6e514dd13db8947884cd58fe2a9c2a063400a9b (diff) |
Add latest changes from gitlab-org/gitlab@14-4-stable-eev14.4.0-rc42
Diffstat (limited to 'app/assets/javascripts/content_editor/extensions')
8 files changed, 271 insertions, 1 deletions
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 25f5837d2a6..1ed1ab0315f 100644 --- a/app/assets/javascripts/content_editor/extensions/code_block_highlight.js +++ b/app/assets/javascripts/content_editor/extensions/code_block_highlight.js @@ -11,7 +11,8 @@ export default CodeBlockLowlight.extend({ parseHTML: (element) => extractLanguage(element), }, class: { - default: 'code highlight js-syntax-highlight', + // eslint-disable-next-line @gitlab/require-i18n-strings + default: 'code highlight', }, }; }, diff --git a/app/assets/javascripts/content_editor/extensions/color_chip.js b/app/assets/javascripts/content_editor/extensions/color_chip.js new file mode 100644 index 00000000000..deb5029a1f0 --- /dev/null +++ b/app/assets/javascripts/content_editor/extensions/color_chip.js @@ -0,0 +1,73 @@ +import { Node } from '@tiptap/core'; +import { Plugin, PluginKey } from 'prosemirror-state'; +import { Decoration, DecorationSet } from 'prosemirror-view'; +import { isValidColorExpression } from '~/lib/utils/color_utils'; +import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants'; + +const colorExpressionTypes = ['#', 'hsl', 'rgb']; + +const isValidColor = (color) => { + if (!colorExpressionTypes.some((type) => color.toLowerCase().startsWith(type))) { + return false; + } + + return isValidColorExpression(color); +}; + +const highlightColors = (doc) => { + const decorations = []; + + doc.descendants((node, position) => { + const { text, marks } = node; + + if (!text || marks.length === 0 || marks[0].type.name !== 'code' || !isValidColor(text)) { + return; + } + + const from = position; + const to = from + text.length; + const decoration = Decoration.inline(from, to, { + class: 'gl-display-inline-flex gl-align-items-center content-editor-color-chip', + style: `--gl-color-chip-color: ${text}`, + }); + + decorations.push(decoration); + }); + + return DecorationSet.create(doc, decorations); +}; + +export const colorDecoratorPlugin = new Plugin({ + key: new PluginKey('colorDecorator'), + state: { + init(_, { doc }) { + return highlightColors(doc); + }, + apply(transaction, oldState) { + return transaction.docChanged ? highlightColors(transaction.doc) : oldState; + }, + }, + props: { + decorations(state) { + return this.getState(state); + }, + }, +}); + +export default Node.create({ + name: 'colorChip', + + parseHTML() { + return [ + { + tag: '.gfm-color_chip', + ignore: true, + priority: PARSE_HTML_PRIORITY_HIGHEST, + }, + ]; + }, + + addProseMirrorPlugins() { + return [colorDecoratorPlugin]; + }, +}); diff --git a/app/assets/javascripts/content_editor/extensions/details.js b/app/assets/javascripts/content_editor/extensions/details.js new file mode 100644 index 00000000000..e3d54ed01fd --- /dev/null +++ b/app/assets/javascripts/content_editor/extensions/details.js @@ -0,0 +1,36 @@ +import { Node } from '@tiptap/core'; +import { VueNodeViewRenderer } from '@tiptap/vue-2'; +import { wrappingInputRule } from 'prosemirror-inputrules'; +import DetailsWrapper from '../components/wrappers/details.vue'; + +export const inputRegex = /^\s*(<details>)$/; + +export default Node.create({ + name: 'details', + content: 'detailsContent+', + // eslint-disable-next-line @gitlab/require-i18n-strings + group: 'block list', + + parseHTML() { + return [{ tag: 'details' }]; + }, + + renderHTML({ HTMLAttributes }) { + return ['ul', HTMLAttributes, 0]; + }, + + addNodeView() { + return VueNodeViewRenderer(DetailsWrapper); + }, + + addInputRules() { + return [wrappingInputRule(inputRegex, this.type)]; + }, + + addCommands() { + return { + setDetails: () => ({ commands }) => commands.wrapInList('details'), + toggleDetails: () => ({ commands }) => commands.toggleList('details', 'detailsContent'), + }; + }, +}); diff --git a/app/assets/javascripts/content_editor/extensions/details_content.js b/app/assets/javascripts/content_editor/extensions/details_content.js new file mode 100644 index 00000000000..fb6c49d91aa --- /dev/null +++ b/app/assets/javascripts/content_editor/extensions/details_content.js @@ -0,0 +1,25 @@ +import { Node } from '@tiptap/core'; +import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants'; + +export default Node.create({ + name: 'detailsContent', + content: 'block+', + defining: true, + + parseHTML() { + return [ + { tag: '*', consuming: false, context: 'details/', priority: PARSE_HTML_PRIORITY_HIGHEST }, + ]; + }, + + renderHTML({ HTMLAttributes }) { + return ['li', HTMLAttributes, 0]; + }, + + addKeyboardShortcuts() { + return { + Enter: () => this.editor.commands.splitListItem('detailsContent'), + 'Shift-Tab': () => this.editor.commands.liftListItem('detailsContent'), + }; + }, +}); diff --git a/app/assets/javascripts/content_editor/extensions/frontmatter.js b/app/assets/javascripts/content_editor/extensions/frontmatter.js new file mode 100644 index 00000000000..64c84fe046b --- /dev/null +++ b/app/assets/javascripts/content_editor/extensions/frontmatter.js @@ -0,0 +1,20 @@ +import { VueNodeViewRenderer } from '@tiptap/vue-2'; +import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants'; +import FrontmatterWrapper from '../components/wrappers/frontmatter.vue'; +import CodeBlockHighlight from './code_block_highlight'; + +export default CodeBlockHighlight.extend({ + name: 'frontmatter', + parseHTML() { + return [ + { + tag: 'pre[data-lang-params="frontmatter"]', + preserveWhitespace: 'full', + priority: PARSE_HTML_PRIORITY_HIGHEST, + }, + ]; + }, + addNodeView() { + return new VueNodeViewRenderer(FrontmatterWrapper); + }, +}); diff --git a/app/assets/javascripts/content_editor/extensions/math_inline.js b/app/assets/javascripts/content_editor/extensions/math_inline.js new file mode 100644 index 00000000000..60f5288dcf6 --- /dev/null +++ b/app/assets/javascripts/content_editor/extensions/math_inline.js @@ -0,0 +1,35 @@ +import { Mark, markInputRule } from '@tiptap/core'; +import { __ } from '~/locale'; +import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants'; + +export const inputRegex = /(?:^|\s)\$`([^`]+)`\$$/gm; + +export default Mark.create({ + name: 'mathInline', + + parseHTML() { + return [ + { + tag: 'code.math[data-math-style=inline]', + priority: PARSE_HTML_PRIORITY_HIGHEST, + }, + ]; + }, + + renderHTML({ HTMLAttributes }) { + return [ + 'code', + { + title: __('Inline math'), + 'data-toggle': 'tooltip', + class: 'gl-inset-border-1-gray-400', + ...HTMLAttributes, + }, + 0, + ]; + }, + + addInputRules() { + return [markInputRule(inputRegex, this.type)]; + }, +}); diff --git a/app/assets/javascripts/content_editor/extensions/table_of_contents.js b/app/assets/javascripts/content_editor/extensions/table_of_contents.js new file mode 100644 index 00000000000..9e31158837e --- /dev/null +++ b/app/assets/javascripts/content_editor/extensions/table_of_contents.js @@ -0,0 +1,51 @@ +import { Node } from '@tiptap/core'; +import { InputRule } from 'prosemirror-inputrules'; +import { s__ } from '~/locale'; +import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants'; + +export const inputRuleRegExps = [/^\[\[_TOC_\]\]$/, /^\[TOC\]$/]; + +export default Node.create({ + name: 'tableOfContents', + + inline: false, + + group: 'block', + + parseHTML() { + return [ + { + tag: 'ul.section-nav', + priority: PARSE_HTML_PRIORITY_HIGHEST, + }, + ]; + }, + + renderHTML() { + return [ + 'div', + { + class: + 'table-of-contents gl-border-1 gl-border-solid gl-text-center gl-border-gray-100 gl-mb-5', + }, + s__('ContentEditor|Table of Contents'), + ]; + }, + + addInputRules() { + const { type } = this; + + return inputRuleRegExps.map( + (regex) => + new InputRule(regex, (state, match, start, end) => { + const { tr } = state; + + if (match) { + tr.replaceWith(start - 1, end, type.create()); + } + + return tr; + }), + ); + }, +}); diff --git a/app/assets/javascripts/content_editor/extensions/word_break.js b/app/assets/javascripts/content_editor/extensions/word_break.js new file mode 100644 index 00000000000..93b42466850 --- /dev/null +++ b/app/assets/javascripts/content_editor/extensions/word_break.js @@ -0,0 +1,29 @@ +import { Node, mergeAttributes, nodeInputRule } from '@tiptap/core'; + +export const inputRegex = /^<wbr>$/; + +export default Node.create({ + name: 'wordBreak', + inline: true, + group: 'inline', + selectable: false, + atom: true, + + defaultOptions: { + HTMLAttributes: { + class: 'gl-display-inline-flex gl-px-1 gl-bg-blue-100 gl-rounded-base gl-font-sm', + }, + }, + + parseHTML() { + return [{ tag: 'wbr' }]; + }, + + renderHTML({ HTMLAttributes }) { + return ['span', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), '-']; + }, + + addInputRules() { + return [nodeInputRule(inputRegex, this.type)]; + }, +}); |