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>2021-10-20 11:43:02 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-10-20 11:43:02 +0300
commitd9ab72d6080f594d0b3cae15f14b3ef2c6c638cb (patch)
tree2341ef426af70ad1e289c38036737e04b0aa5007 /app/assets/javascripts/content_editor
parentd6e514dd13db8947884cd58fe2a9c2a063400a9b (diff)
Add latest changes from gitlab-org/gitlab@14-4-stable-eev14.4.0-rc42
Diffstat (limited to 'app/assets/javascripts/content_editor')
-rw-r--r--app/assets/javascripts/content_editor/components/top_toolbar.vue9
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/details.vue33
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/frontmatter.vue32
-rw-r--r--app/assets/javascripts/content_editor/content_editor.stories.js2
-rw-r--r--app/assets/javascripts/content_editor/extensions/code_block_highlight.js3
-rw-r--r--app/assets/javascripts/content_editor/extensions/color_chip.js73
-rw-r--r--app/assets/javascripts/content_editor/extensions/details.js36
-rw-r--r--app/assets/javascripts/content_editor/extensions/details_content.js25
-rw-r--r--app/assets/javascripts/content_editor/extensions/frontmatter.js20
-rw-r--r--app/assets/javascripts/content_editor/extensions/math_inline.js35
-rw-r--r--app/assets/javascripts/content_editor/extensions/table_of_contents.js51
-rw-r--r--app/assets/javascripts/content_editor/extensions/word_break.js29
-rw-r--r--app/assets/javascripts/content_editor/services/create_content_editor.js14
-rw-r--r--app/assets/javascripts/content_editor/services/markdown_serializer.js40
14 files changed, 400 insertions, 2 deletions
diff --git a/app/assets/javascripts/content_editor/components/top_toolbar.vue b/app/assets/javascripts/content_editor/components/top_toolbar.vue
index 82a449ae6af..89182b3a09f 100644
--- a/app/assets/javascripts/content_editor/components/top_toolbar.vue
+++ b/app/assets/javascripts/content_editor/components/top_toolbar.vue
@@ -112,6 +112,15 @@ export default {
@execute="trackToolbarControlExecution"
/>
<toolbar-button
+ data-testid="details"
+ content-type="details"
+ icon-name="details-block"
+ class="gl-mx-2"
+ editor-command="toggleDetails"
+ :label="__('Add a collapsible section')"
+ @execute="trackToolbarControlExecution"
+ />
+ <toolbar-button
data-testid="horizontal-rule"
content-type="horizontalRule"
icon-name="dash"
diff --git a/app/assets/javascripts/content_editor/components/wrappers/details.vue b/app/assets/javascripts/content_editor/components/wrappers/details.vue
new file mode 100644
index 00000000000..aff15ac3e53
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/wrappers/details.vue
@@ -0,0 +1,33 @@
+<script>
+import { NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2';
+
+export default {
+ name: 'DetailsWrapper',
+ components: {
+ NodeViewWrapper,
+ NodeViewContent,
+ },
+ props: {
+ node: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ open: true,
+ };
+ },
+};
+</script>
+<template>
+ <node-view-wrapper class="gl-display-flex">
+ <div
+ class="details-toggle-icon"
+ data-testid="details-toggle-icon"
+ :class="{ 'is-open': open }"
+ @click="open = !open"
+ ></div>
+ <node-view-content as="ul" class="details-content" :class="{ 'is-open': open }" />
+ </node-view-wrapper>
+</template>
diff --git a/app/assets/javascripts/content_editor/components/wrappers/frontmatter.vue b/app/assets/javascripts/content_editor/components/wrappers/frontmatter.vue
new file mode 100644
index 00000000000..97b69afd12e
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/wrappers/frontmatter.vue
@@ -0,0 +1,32 @@
+<script>
+import { NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2';
+import { __ } from '~/locale';
+
+export default {
+ name: 'FrontMatter',
+ components: {
+ NodeViewWrapper,
+ NodeViewContent,
+ },
+ props: {
+ node: {
+ type: Object,
+ required: true,
+ },
+ },
+ i18n: {
+ frontmatter: __('frontmatter'),
+ },
+};
+</script>
+<template>
+ <node-view-wrapper class="gl-relative code highlight" as="pre">
+ <span
+ data-testid="frontmatter-label"
+ class="gl-absolute gl-top-0 gl-right-3"
+ contenteditable="false"
+ >{{ $options.i18n.frontmatter }}:{{ node.attrs.language }}</span
+ >
+ <node-view-content as="code" />
+ </node-view-wrapper>
+</template>
diff --git a/app/assets/javascripts/content_editor/content_editor.stories.js b/app/assets/javascripts/content_editor/content_editor.stories.js
index 8f2ce8feb5d..9329bbcb2c7 100644
--- a/app/assets/javascripts/content_editor/content_editor.stories.js
+++ b/app/assets/javascripts/content_editor/content_editor.stories.js
@@ -2,7 +2,7 @@ import { ContentEditor } from './index';
export default {
component: ContentEditor,
- title: 'Components/Content Editor',
+ title: 'content_editor/components/content_editor',
};
const Template = (_, { argTypes }) => ({
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)];
+ },
+});
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 9b2d4c9a062..385f1c63801 100644
--- a/app/assets/javascripts/content_editor/services/create_content_editor.js
+++ b/app/assets/javascripts/content_editor/services/create_content_editor.js
@@ -8,14 +8,18 @@ import Bold from '../extensions/bold';
import BulletList from '../extensions/bullet_list';
import Code from '../extensions/code';
import CodeBlockHighlight from '../extensions/code_block_highlight';
+import ColorChip from '../extensions/color_chip';
import DescriptionItem from '../extensions/description_item';
import DescriptionList from '../extensions/description_list';
+import Details from '../extensions/details';
+import DetailsContent from '../extensions/details_content';
import Division from '../extensions/division';
import Document from '../extensions/document';
import Dropcursor from '../extensions/dropcursor';
import Emoji from '../extensions/emoji';
import Figure from '../extensions/figure';
import FigureCaption from '../extensions/figure_caption';
+import Frontmatter from '../extensions/frontmatter';
import Gapcursor from '../extensions/gapcursor';
import HardBreak from '../extensions/hard_break';
import Heading from '../extensions/heading';
@@ -28,6 +32,7 @@ import Italic from '../extensions/italic';
import Link from '../extensions/link';
import ListItem from '../extensions/list_item';
import Loading from '../extensions/loading';
+import MathInline from '../extensions/math_inline';
import OrderedList from '../extensions/ordered_list';
import Paragraph from '../extensions/paragraph';
import Reference from '../extensions/reference';
@@ -37,11 +42,13 @@ import Superscript from '../extensions/superscript';
import Table from '../extensions/table';
import TableCell from '../extensions/table_cell';
import TableHeader from '../extensions/table_header';
+import TableOfContents from '../extensions/table_of_contents';
import TableRow from '../extensions/table_row';
import TaskItem from '../extensions/task_item';
import TaskList from '../extensions/task_list';
import Text from '../extensions/text';
import Video from '../extensions/video';
+import WordBreak from '../extensions/word_break';
import { ContentEditor } from './content_editor';
import createMarkdownSerializer from './markdown_serializer';
import trackInputRulesAndShortcuts from './track_input_rules_and_shortcuts';
@@ -75,15 +82,19 @@ export const createContentEditor = ({
Bold,
BulletList,
Code,
+ ColorChip,
CodeBlockHighlight,
DescriptionItem,
DescriptionList,
+ Details,
+ DetailsContent,
Document,
Division,
Dropcursor,
Emoji,
Figure,
FigureCaption,
+ Frontmatter,
Gapcursor,
HardBreak,
Heading,
@@ -96,6 +107,7 @@ export const createContentEditor = ({
Link,
ListItem,
Loading,
+ MathInline,
OrderedList,
Paragraph,
Reference,
@@ -104,12 +116,14 @@ export const createContentEditor = ({
Superscript,
TableCell,
TableHeader,
+ TableOfContents,
TableRow,
Table,
TaskItem,
TaskList,
Text,
Video,
+ WordBreak,
];
const allExtensions = [...builtInContentEditorExtensions, ...extensions];
diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js
index bc6d98511f9..0dd3cb5b73f 100644
--- a/app/assets/javascripts/content_editor/services/markdown_serializer.js
+++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js
@@ -11,10 +11,13 @@ import Code from '../extensions/code';
import CodeBlockHighlight from '../extensions/code_block_highlight';
import DescriptionItem from '../extensions/description_item';
import DescriptionList from '../extensions/description_list';
+import Details from '../extensions/details';
+import DetailsContent from '../extensions/details_content';
import Division from '../extensions/division';
import Emoji from '../extensions/emoji';
import Figure from '../extensions/figure';
import FigureCaption from '../extensions/figure_caption';
+import Frontmatter from '../extensions/frontmatter';
import HardBreak from '../extensions/hard_break';
import Heading from '../extensions/heading';
import HorizontalRule from '../extensions/horizontal_rule';
@@ -24,6 +27,7 @@ import InlineDiff from '../extensions/inline_diff';
import Italic from '../extensions/italic';
import Link from '../extensions/link';
import ListItem from '../extensions/list_item';
+import MathInline from '../extensions/math_inline';
import OrderedList from '../extensions/ordered_list';
import Paragraph from '../extensions/paragraph';
import Reference from '../extensions/reference';
@@ -33,11 +37,13 @@ import Superscript from '../extensions/superscript';
import Table from '../extensions/table';
import TableCell from '../extensions/table_cell';
import TableHeader from '../extensions/table_header';
+import TableOfContents from '../extensions/table_of_contents';
import TableRow from '../extensions/table_row';
import TaskItem from '../extensions/task_item';
import TaskList from '../extensions/task_list';
import Text from '../extensions/text';
import Video from '../extensions/video';
+import WordBreak from '../extensions/word_break';
import {
isPlainURL,
renderHardBreak,
@@ -50,6 +56,7 @@ import {
renderImage,
renderPlayable,
renderHTMLNode,
+ renderContent,
} from './serialization_helpers';
const defaultSerializerConfig = {
@@ -80,6 +87,11 @@ const defaultSerializerConfig = {
: `](${state.esc(href)}${mark.attrs.title ? ` ${state.quote(mark.attrs.title)}` : ''})`;
},
},
+ [MathInline.name]: {
+ open: (...args) => `$${defaultMarkdownSerializer.marks.code.open(...args)}`,
+ close: (...args) => `${defaultMarkdownSerializer.marks.code.close(...args)}$`,
+ escape: false,
+ },
[Strike.name]: {
open: '~~',
close: '~~',
@@ -130,11 +142,34 @@ const defaultSerializerConfig = {
renderHTMLNode(node.attrs.isTerm ? 'dt' : 'dd')(state, node);
if (index === parent.childCount - 1) state.ensureNewLine();
},
+ [Details.name]: renderHTMLNode('details', true),
+ [DetailsContent.name]: (state, node, parent, index) => {
+ if (!index) renderHTMLNode('summary')(state, node);
+ else {
+ if (index === 1) state.ensureNewLine();
+ renderContent(state, node);
+ if (index === parent.childCount - 1) state.ensureNewLine();
+ }
+ },
[Emoji.name]: (state, node) => {
const { name } = node.attrs;
state.write(`:${name}:`);
},
+ [Frontmatter.name]: (state, node) => {
+ const { language } = node.attrs;
+ const syntax = {
+ toml: '+++',
+ json: ';;;',
+ yaml: '---',
+ }[language];
+
+ state.write(`${syntax}\n`);
+ state.text(node.textContent, false);
+ state.ensureNewLine();
+ state.write(syntax);
+ state.closeBlock(node);
+ },
[Figure.name]: renderHTMLNode('figure'),
[FigureCaption.name]: renderHTMLNode('figcaption'),
[HardBreak.name]: renderHardBreak,
@@ -147,6 +182,10 @@ const defaultSerializerConfig = {
[Reference.name]: (state, node) => {
state.write(node.attrs.originalText || node.attrs.text);
},
+ [TableOfContents.name]: (state, node) => {
+ state.write('[[_TOC_]]');
+ state.closeBlock(node);
+ },
[Table.name]: renderTable,
[TableCell.name]: renderTableCell,
[TableHeader.name]: renderTableCell,
@@ -161,6 +200,7 @@ const defaultSerializerConfig = {
},
[Text.name]: defaultMarkdownSerializer.nodes.text,
[Video.name]: renderPlayable,
+ [WordBreak.name]: (state) => state.write('<wbr>'),
},
};