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>2024-01-20 03:09:13 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2024-01-20 03:09:13 +0300
commitc0bc64e25edb4d7c3ac1d89de720f94782be5d2e (patch)
tree7cce86fcd83c9ba6fe7c3eb418f19a982d6eff3d
parent1cd61065a0d86b492be5086906429ac5956e3672 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/behaviors/markdown/copy_as_gfm.js10
-rw-r--r--app/assets/javascripts/behaviors/markdown/editor_extensions.js95
-rw-r--r--app/assets/javascripts/behaviors/markdown/marks/bold.js17
-rw-r--r--app/assets/javascripts/behaviors/markdown/marks/code.js12
-rw-r--r--app/assets/javascripts/behaviors/markdown/marks/inline_diff.js29
-rw-r--r--app/assets/javascripts/behaviors/markdown/marks/inline_html.js35
-rw-r--r--app/assets/javascripts/behaviors/markdown/marks/italic.js11
-rw-r--r--app/assets/javascripts/behaviors/markdown/marks/link.js47
-rw-r--r--app/assets/javascripts/behaviors/markdown/marks/math.js31
-rw-r--r--app/assets/javascripts/behaviors/markdown/marks/strike.js37
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/audio.js4
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/blockquote.js19
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/bullet_list.js15
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/code_block.js94
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/description_details.js20
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/description_list.js19
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/description_term.js18
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/details.js18
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/doc.js6
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/emoji.js43
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/hard_break.js14
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/heading.js27
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/horizontal_rule.js14
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/image.js48
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/list_item.js16
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/ordered_list.js25
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/ordered_task_list.js21
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/paragraph.js15
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/playable.js56
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/reference.js44
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/summary.js17
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/table.js15
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/table_body.js14
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/table_cell.js25
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/table_head.js14
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/table_header_row.js36
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js26
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/table_row.js28
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/task_list.js20
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js71
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/text.js11
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/video.js4
-rw-r--r--app/assets/javascripts/behaviors/markdown/schema.js25
-rw-r--r--app/assets/javascripts/behaviors/markdown/serializer.js21
-rw-r--r--app/assets/javascripts/content_editor/extensions/code_block_highlight.js42
-rw-r--r--app/assets/javascripts/content_editor/extensions/code_suggestion.js11
-rw-r--r--app/assets/javascripts/content_editor/extensions/emoji.js40
-rw-r--r--app/assets/javascripts/content_editor/extensions/index.js62
-rw-r--r--app/assets/javascripts/content_editor/extensions/inline_diff.js8
-rw-r--r--app/assets/javascripts/content_editor/extensions/math_inline.js10
-rw-r--r--app/assets/javascripts/content_editor/extensions/strike.js23
-rw-r--r--app/assets/javascripts/content_editor/extensions/task_item.js2
-rw-r--r--app/assets/javascripts/content_editor/services/create_content_editor.js139
-rw-r--r--app/assets/javascripts/content_editor/services/markdown_serializer.js173
-rw-r--r--app/assets/javascripts/content_editor/services/serialization_helpers.js79
-rw-r--r--app/models/organizations/organization_setting.rb8
-rw-r--r--glfm_specification/output_example_snapshots/prosemirror_json.yml45
-rw-r--r--lib/gitlab/current_settings.rb10
-rw-r--r--lib/gitlab/fake_application_settings.rb4
-rw-r--r--locale/gitlab.pot3
-rw-r--r--spec/features/markdown/copy_as_gfm_spec.rb47
-rw-r--r--spec/frontend/behaviors/copy_as_gfm_spec.js4
-rw-r--r--spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js4
-rw-r--r--spec/frontend/content_editor/extensions/code_suggestion_spec.js6
-rw-r--r--spec/frontend/content_editor/services/markdown_serializer_spec.js95
-rw-r--r--spec/lib/gitlab/current_settings_spec.rb56
-rw-r--r--spec/models/organizations/organization_setting_spec.rb38
-rw-r--r--spec/scripts/lib/glfm/update_example_snapshots_spec.rb5
68 files changed, 606 insertions, 1495 deletions
diff --git a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
index 72aae254584..7e5a51a825f 100644
--- a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
+++ b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
@@ -161,6 +161,8 @@ export class CopyAsGFM {
codeElement.appendChild(documentFragment);
}
+ [...codeElement.querySelectorAll('.idiff')].forEach((el) => el.classList.remove('idiff'));
+
return codeElement;
}
@@ -176,10 +178,10 @@ export class CopyAsGFM {
wrapEl.appendChild(node.cloneNode(true));
const doc = DOMParser.fromSchema(schema.default).parse(wrapEl);
- const res = markdownSerializer.default.serialize(doc, {
- tightLists: true,
- });
- return res;
+ return markdownSerializer.default.serialize(
+ { doc },
+ { useCanonicalSrc: false, skipEmptyNodes: true },
+ );
})
.catch(() => {});
}
diff --git a/app/assets/javascripts/behaviors/markdown/editor_extensions.js b/app/assets/javascripts/behaviors/markdown/editor_extensions.js
deleted file mode 100644
index 165031c3e7d..00000000000
--- a/app/assets/javascripts/behaviors/markdown/editor_extensions.js
+++ /dev/null
@@ -1,95 +0,0 @@
-import Bold from './marks/bold';
-import Code from './marks/code';
-import InlineDiff from './marks/inline_diff';
-import InlineHTML from './marks/inline_html';
-import Italic from './marks/italic';
-import Link from './marks/link';
-import MathMark from './marks/math';
-import Strike from './marks/strike';
-import Audio from './nodes/audio';
-import Blockquote from './nodes/blockquote';
-import BulletList from './nodes/bullet_list';
-import CodeBlock from './nodes/code_block';
-import DescriptionDetails from './nodes/description_details';
-import DescriptionList from './nodes/description_list';
-import DescriptionTerm from './nodes/description_term';
-import Details from './nodes/details';
-import Doc from './nodes/doc';
-
-import Emoji from './nodes/emoji';
-import HardBreak from './nodes/hard_break';
-import Heading from './nodes/heading';
-import HorizontalRule from './nodes/horizontal_rule';
-import Image from './nodes/image';
-
-import ListItem from './nodes/list_item';
-import OrderedList from './nodes/ordered_list';
-import OrderedTaskList from './nodes/ordered_task_list';
-import Paragraph from './nodes/paragraph';
-import Reference from './nodes/reference';
-import Summary from './nodes/summary';
-import Table from './nodes/table';
-import TableBody from './nodes/table_body';
-import TableCell from './nodes/table_cell';
-import TableHead from './nodes/table_head';
-import TableHeaderRow from './nodes/table_header_row';
-import TableOfContents from './nodes/table_of_contents';
-import TableRow from './nodes/table_row';
-
-import TaskList from './nodes/task_list';
-import TaskListItem from './nodes/task_list_item';
-import Text from './nodes/text';
-import Video from './nodes/video';
-
-// The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb transform
-// GitLab Flavored Markdown (GFM) to HTML.
-// The nodes and marks referenced here transform that same HTML to GFM to be copied to the clipboard.
-// Every filter in lib/banzai/pipeline/gfm_pipeline.rb that generates HTML
-// from GFM should have a node or mark here.
-// The GFM-to-HTML-to-GFM cycle is tested in spec/features/markdown/copy_as_gfm_spec.rb.
-
-export default {
- nodes: [
- Doc(),
- Paragraph(),
- Text(),
-
- Blockquote(),
- CodeBlock(),
- HardBreak(),
- Heading(),
- HorizontalRule(),
- Image(),
-
- Table(),
- TableHead(),
- TableBody(),
- TableHeaderRow(),
- TableRow(),
- TableCell(),
-
- Emoji(),
- Reference(),
-
- TableOfContents(),
- Video(),
- Audio(),
-
- BulletList(),
- OrderedList(),
- ListItem(),
-
- DescriptionList(),
- DescriptionTerm(),
- DescriptionDetails(),
-
- TaskList(),
- OrderedTaskList(),
- TaskListItem(),
-
- Summary(),
- Details(),
- ],
-
- marks: [Bold(), Italic(), Strike(), InlineDiff(), Link(), Code(), MathMark(), InlineHTML()],
-};
diff --git a/app/assets/javascripts/behaviors/markdown/marks/bold.js b/app/assets/javascripts/behaviors/markdown/marks/bold.js
deleted file mode 100644
index dd730947a5f..00000000000
--- a/app/assets/javascripts/behaviors/markdown/marks/bold.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import { defaultMarkdownSerializer } from '~/lib/prosemirror_markdown_serializer';
-
-// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
-export default () => {
- return {
- name: 'bold',
- schema: {
- parseDOM: [
- {
- tag: 'strong',
- },
- ],
- toDOM: () => ['strong', 0],
- },
- toMarkdown: defaultMarkdownSerializer.marks.strong,
- };
-};
diff --git a/app/assets/javascripts/behaviors/markdown/marks/code.js b/app/assets/javascripts/behaviors/markdown/marks/code.js
deleted file mode 100644
index ea5af8b4a1f..00000000000
--- a/app/assets/javascripts/behaviors/markdown/marks/code.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import { defaultMarkdownSerializer } from '~/lib/prosemirror_markdown_serializer';
-
-// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
-export default () => ({
- name: 'code',
- schema: {
- excludes: '_',
- parseDOM: [{ tag: 'code' }],
- toDOM: () => ['code', 0],
- },
- toMarkdown: defaultMarkdownSerializer.marks.code,
-});
diff --git a/app/assets/javascripts/behaviors/markdown/marks/inline_diff.js b/app/assets/javascripts/behaviors/markdown/marks/inline_diff.js
deleted file mode 100644
index 69d345c81e4..00000000000
--- a/app/assets/javascripts/behaviors/markdown/marks/inline_diff.js
+++ /dev/null
@@ -1,29 +0,0 @@
-// Transforms generated HTML back to GFM for Banzai::Filter::InlineDiffFilter
-export default () => ({
- name: 'inline_diff',
- schema: {
- attrs: {
- addition: {
- default: true,
- },
- },
- parseDOM: [
- { tag: 'span.idiff.addition', attrs: { addition: true } },
- { tag: 'span.idiff.deletion', attrs: { addition: false } },
- ],
- toDOM: (node) => [
- 'span',
- { class: `idiff left right ${node.attrs.addition ? 'addition' : 'deletion'}` },
- 0,
- ],
- },
- toMarkdown: {
- mixable: true,
- open(_, mark) {
- return mark.attrs.addition ? '{+' : '{-';
- },
- close(_, mark) {
- return mark.attrs.addition ? '+}' : '-}';
- },
- },
-});
diff --git a/app/assets/javascripts/behaviors/markdown/marks/inline_html.js b/app/assets/javascripts/behaviors/markdown/marks/inline_html.js
deleted file mode 100644
index 4520598e0ab..00000000000
--- a/app/assets/javascripts/behaviors/markdown/marks/inline_html.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import { escape } from 'lodash';
-
-// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
-export default () => ({
- name: 'inline_html',
- schema: {
- excludes: '',
- attrs: {
- tag: {},
- title: { default: null },
- },
- parseDOM: [
- {
- tag: 'sup, sub, kbd, q, samp, var',
- getAttrs: (el) => ({ tag: el.nodeName.toLowerCase() }),
- },
- {
- tag: 'abbr',
- getAttrs: (el) => ({ tag: 'abbr', title: el.getAttribute('title') }),
- },
- ],
- toDOM: (node) => [node.attrs.tag, { title: node.attrs.title }, 0],
- },
- toMarkdown: {
- mixable: true,
- open(state, mark) {
- return `<${mark.attrs.tag}${
- mark.attrs.title ? ` title="${state.esc(escape(mark.attrs.title))}"` : ''
- }>`;
- },
- close(_, mark) {
- return `</${mark.attrs.tag}>`;
- },
- },
-});
diff --git a/app/assets/javascripts/behaviors/markdown/marks/italic.js b/app/assets/javascripts/behaviors/markdown/marks/italic.js
deleted file mode 100644
index 3ec8f0071e9..00000000000
--- a/app/assets/javascripts/behaviors/markdown/marks/italic.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import { defaultMarkdownSerializer } from '~/lib/prosemirror_markdown_serializer';
-
-// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
-export default () => ({
- name: 'italic',
- schema: {
- parseDOM: [{ tag: 'em' }],
- toDOM: () => ['em', 0],
- },
- toMarkdown: defaultMarkdownSerializer.marks.em,
-});
diff --git a/app/assets/javascripts/behaviors/markdown/marks/link.js b/app/assets/javascripts/behaviors/markdown/marks/link.js
deleted file mode 100644
index 977453fee01..00000000000
--- a/app/assets/javascripts/behaviors/markdown/marks/link.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import { defaultMarkdownSerializer } from '~/lib/prosemirror_markdown_serializer';
-
-// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
-export default () => ({
- name: 'link',
- schema: {
- attrs: {
- href: {
- default: null,
- },
- target: {
- default: null,
- },
- },
- inclusive: false,
- parseDOM: [
- {
- tag: 'a[href]',
- getAttrs: (dom) => ({
- href: dom.getAttribute('href'),
- target: dom.getAttribute('target'),
- }),
- },
- ],
- toDOM: (node) => [
- 'a',
- {
- ...node.attrs,
- // eslint-disable-next-line @gitlab/require-i18n-strings
- rel: 'noopener noreferrer nofollow',
- target: node.attrs.target,
- },
- 0,
- ],
- },
- toMarkdown: {
- mixable: true,
- open(state, mark, parent, index) {
- const open = defaultMarkdownSerializer.marks.link.open(state, mark, parent, index);
- return open === '<' ? '' : open;
- },
- close(state, mark, parent, index) {
- const close = defaultMarkdownSerializer.marks.link.close(state, mark, parent, index);
- return close === '>' ? '' : close;
- },
- },
-});
diff --git a/app/assets/javascripts/behaviors/markdown/marks/math.js b/app/assets/javascripts/behaviors/markdown/marks/math.js
deleted file mode 100644
index a50a649b6eb..00000000000
--- a/app/assets/javascripts/behaviors/markdown/marks/math.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import { defaultMarkdownSerializer } from '~/lib/prosemirror_markdown_serializer';
-import { HIGHER_PARSE_RULE_PRIORITY } from '../constants';
-
-// Transforms generated HTML back to GFM for Banzai::Filter::MathFilter
-export default () => ({
- name: 'math',
- schema: {
- parseDOM: [
- // Matches HTML generated by Banzai::Filter::MathFilter
- {
- tag: 'code.code.math[data-math-style=inline]',
- priority: HIGHER_PARSE_RULE_PRIORITY,
- },
- // Matches HTML after being transformed by app/assets/javascripts/behaviors/markdown/render_math.js
- {
- tag: 'span.katex',
- contentElement: 'annotation[encoding="application/x-tex"]',
- },
- ],
- toDOM: () => ['code', { class: 'code math', 'data-math-style': 'inline' }, 0],
- },
- toMarkdown: {
- escape: false,
- open(state, mark, parent, index) {
- return `$${defaultMarkdownSerializer.marks.code.open(state, mark, parent, index)}`;
- },
- close(state, mark, parent, index) {
- return `${defaultMarkdownSerializer.marks.code.close(state, mark, parent, index)}$`;
- },
- },
-});
diff --git a/app/assets/javascripts/behaviors/markdown/marks/strike.js b/app/assets/javascripts/behaviors/markdown/marks/strike.js
deleted file mode 100644
index afab266b645..00000000000
--- a/app/assets/javascripts/behaviors/markdown/marks/strike.js
+++ /dev/null
@@ -1,37 +0,0 @@
-// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
-export default () => ({
- name: 'strike',
- schema: {
- attrs: {
- strike: {
- default: false,
- },
- inapplicable: {
- default: false,
- },
- },
- parseDOM: [
- { tag: 'li.inapplicable > s', attrs: { inapplicable: true } },
- { tag: 'li.inapplicable > p:first-of-type > s', attrs: { inapplicable: true } },
- { tag: 's', attrs: { strike: true } },
- { tag: 'del' },
- ],
- toDOM: () => ['s', 0],
- },
- toMarkdown: {
- open(_, mark) {
- if (mark.attrs.strike) {
- return '<s>';
- }
- return mark.attrs.inapplicable ? '' : '~~';
- },
- close(_, mark) {
- if (mark.attrs.strike) {
- return '</s>';
- }
- return mark.attrs.inapplicable ? '' : '~~';
- },
- mixable: true,
- expelEnclosingWhitespace: true,
- },
-});
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/audio.js b/app/assets/javascripts/behaviors/markdown/nodes/audio.js
deleted file mode 100644
index 97ab86c6d23..00000000000
--- a/app/assets/javascripts/behaviors/markdown/nodes/audio.js
+++ /dev/null
@@ -1,4 +0,0 @@
-import playable from './playable';
-
-// Transforms generated HTML back to GFM for Banzai::Filter::AudioLinkFilter
-export default () => playable({ mediaType: 'audio' });
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/blockquote.js b/app/assets/javascripts/behaviors/markdown/nodes/blockquote.js
deleted file mode 100644
index 6a4552d47e4..00000000000
--- a/app/assets/javascripts/behaviors/markdown/nodes/blockquote.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import { defaultMarkdownSerializer } from '~/lib/prosemirror_markdown_serializer';
-
-// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
-export default () => ({
- name: 'blockquote',
- schema: {
- content: 'block*',
- group: 'block',
- defining: true,
- draggable: false,
- parseDOM: [{ tag: 'blockquote' }],
- toDOM: () => ['blockquote', 0],
- },
- toMarkdown(state, node) {
- if (!node.childCount) return;
-
- defaultMarkdownSerializer.nodes.blockquote(state, node);
- },
-});
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/bullet_list.js b/app/assets/javascripts/behaviors/markdown/nodes/bullet_list.js
deleted file mode 100644
index 95cd3605da5..00000000000
--- a/app/assets/javascripts/behaviors/markdown/nodes/bullet_list.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import { defaultMarkdownSerializer } from '~/lib/prosemirror_markdown_serializer';
-
-// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
-export default () => ({
- name: 'bullet_list',
- schema: {
- content: 'list_item+',
- group: 'block',
- parseDOM: [{ tag: 'ul' }],
- toDOM: () => ['ul', 0],
- },
- toMarkdown(state, node) {
- defaultMarkdownSerializer.nodes.bullet_list(state, node);
- },
-});
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/code_block.js b/app/assets/javascripts/behaviors/markdown/nodes/code_block.js
deleted file mode 100644
index b862d111de7..00000000000
--- a/app/assets/javascripts/behaviors/markdown/nodes/code_block.js
+++ /dev/null
@@ -1,94 +0,0 @@
-const PLAINTEXT_LANG = 'plaintext';
-
-// Transforms generated HTML back to GFM for:
-// - Banzai::Filter::SyntaxHighlightFilter
-// - Banzai::Filter::MathFilter
-// - Banzai::Filter::MermaidFilter
-// - Banzai::Filter::SuggestionFilter
-export default () => ({
- name: 'code_block',
- schema: {
- content: 'text*',
- marks: '',
- group: 'block',
- code: true,
- defining: true,
- attrs: {
- lang: { default: PLAINTEXT_LANG },
- },
- parseDOM: [
- // Matches HTML generated by Banzai::Filter::SyntaxHighlightFilter, Banzai::Filter::MathFilter, Banzai::Filter::MermaidFilter, or Banzai::Filter::SuggestionFilter
- {
- tag: 'pre.code.highlight',
- preserveWhitespace: 'full',
- getAttrs: (el) => {
- const lang = el.getAttribute('lang');
- if (!lang || lang === '') return {};
-
- return { lang };
- },
- },
- // Matches HTML generated by Banzai::Filter::MathFilter,
- // after being transformed by app/assets/javascripts/behaviors/markdown/render_math.js
- {
- tag: 'span.katex-display',
- preserveWhitespace: 'full',
- contentElement: 'annotation[encoding="application/x-tex"]',
- attrs: { lang: 'math' },
- },
- // Matches HTML generated by Banzai::Filter::MermaidFilter,
- // after being transformed by app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js
- {
- tag: 'svg.mermaid',
- preserveWhitespace: 'full',
- contentElement: 'text.source',
- attrs: { lang: 'mermaid' },
- },
- // Matches HTML generated by Banzai::Filter::SuggestionFilter,
- // after being transformed by app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
- {
- tag: '.md-suggestion',
- skip: true,
- },
- {
- tag: '.md-suggestion-header',
- ignore: true,
- },
- {
- tag: '.md-suggestion-diff',
- preserveWhitespace: 'full',
- getContent: (el, schema) =>
- [...el.querySelectorAll('.line_content.new span')].map((span) =>
- schema.text(span.innerText),
- ),
- attrs: { lang: 'suggestion' },
- },
- ],
- toDOM: (node) => ['pre', { class: 'code highlight', lang: node.attrs.lang }, ['code', 0]],
- },
-
- toMarkdown(state, node) {
- if (!node.childCount) return;
-
- const {
- textContent: text,
- attrs: { lang },
- } = node;
-
- // Prefixes lines with 4 spaces if the code contains a line that starts with triple backticks
- if (lang === PLAINTEXT_LANG && text.match(/^```/gm)) {
- state.wrapBlock(' ', null, node, () => state.text(text, false));
- return;
- }
-
- state.write('```');
- if (lang !== PLAINTEXT_LANG) state.write(lang);
-
- state.ensureNewLine();
- state.text(text, false);
- state.ensureNewLine();
-
- state.write('```');
- state.closeBlock(node);
- },
-});
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/description_details.js b/app/assets/javascripts/behaviors/markdown/nodes/description_details.js
deleted file mode 100644
index 20760286045..00000000000
--- a/app/assets/javascripts/behaviors/markdown/nodes/description_details.js
+++ /dev/null
@@ -1,20 +0,0 @@
-// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
-export default () => ({
- name: 'description_details',
-
- schema: {
- content: 'text*',
- marks: '',
- defining: true,
- parseDOM: [{ tag: 'dd' }],
- toDOM: () => ['dd', 0],
- },
-
- toMarkdown(state, node) {
- state.flushClose(1);
- state.write('<dd>');
- state.text(node.textContent, false);
- state.write('</dd>');
- state.closeBlock(node);
- },
-});
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/description_list.js b/app/assets/javascripts/behaviors/markdown/nodes/description_list.js
deleted file mode 100644
index c5305c48423..00000000000
--- a/app/assets/javascripts/behaviors/markdown/nodes/description_list.js
+++ /dev/null
@@ -1,19 +0,0 @@
-// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
-export default () => ({
- name: 'description_list',
- schema: {
- content: '(description_term+ description_details+)+',
- group: 'block',
- parseDOM: [{ tag: 'dl' }],
- toDOM: () => ['dl', 0],
- },
-
- toMarkdown(state, node) {
- state.write('<dl>\n');
- state.wrapBlock(' ', null, node, () => state.renderContent(node));
- state.flushClose(1);
- state.ensureNewLine();
- state.write('</dl>');
- state.closeBlock(node);
- },
-});
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/description_term.js b/app/assets/javascripts/behaviors/markdown/nodes/description_term.js
deleted file mode 100644
index f78f7f13fc4..00000000000
--- a/app/assets/javascripts/behaviors/markdown/nodes/description_term.js
+++ /dev/null
@@ -1,18 +0,0 @@
-// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
-export default () => ({
- name: 'description_term',
- schema: {
- content: 'text*',
- marks: '',
- defining: true,
- parseDOM: [{ tag: 'dt' }],
- toDOM: () => ['dt', 0],
- },
- toMarkdown(state, node) {
- state.flushClose(state.closed && state.closed.type === node.type ? 1 : 2);
- state.write('<dt>');
- state.text(node.textContent, false);
- state.write('</dt>');
- state.closeBlock(node);
- },
-});
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/details.js b/app/assets/javascripts/behaviors/markdown/nodes/details.js
deleted file mode 100644
index 9fb0d60b93a..00000000000
--- a/app/assets/javascripts/behaviors/markdown/nodes/details.js
+++ /dev/null
@@ -1,18 +0,0 @@
-// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
-export default () => ({
- name: 'details',
- schema: {
- content: 'summary block*',
- group: 'block',
- parseDOM: [{ tag: 'details' }],
- toDOM: () => ['details', { open: true, onclick: 'return false', tabindex: '-1' }, 0],
- },
- toMarkdown(state, node) {
- state.write('<details>\n');
- state.renderContent(node);
- state.flushClose(1);
- state.ensureNewLine();
- state.write('</details>');
- state.closeBlock(node);
- },
-});
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/doc.js b/app/assets/javascripts/behaviors/markdown/nodes/doc.js
deleted file mode 100644
index 3101e6e0e3a..00000000000
--- a/app/assets/javascripts/behaviors/markdown/nodes/doc.js
+++ /dev/null
@@ -1,6 +0,0 @@
-export default () => ({
- name: 'doc',
- schema: {
- content: 'block+',
- },
-});
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/emoji.js b/app/assets/javascripts/behaviors/markdown/nodes/emoji.js
deleted file mode 100644
index 086c277bad4..00000000000
--- a/app/assets/javascripts/behaviors/markdown/nodes/emoji.js
+++ /dev/null
@@ -1,43 +0,0 @@
-// Transforms generated HTML back to GFM for Banzai::Filter::EmojiFilter
-export default () => ({
- name: 'emoji',
- schema: {
- inline: true,
- group: 'inline',
- attrs: {
- name: {},
- title: {},
- moji: {},
- },
- parseDOM: [
- {
- tag: 'gl-emoji',
- getAttrs: (el) => ({
- name: el.dataset.name,
- title: el.getAttribute('title'),
- moji: el.textContent,
- }),
- },
- {
- tag: 'img.emoji',
- getAttrs: (el) => {
- const name = el.getAttribute('title').replace(/^:|:$/g, '');
-
- return {
- name,
- title: name,
- moji: name,
- };
- },
- },
- ],
- toDOM: (node) => [
- 'gl-emoji',
- { 'data-name': node.attrs.name, title: node.attrs.title },
- node.attrs.moji,
- ],
- },
- toMarkdown(state, node) {
- state.write(`:${node.attrs.name}:`);
- },
-});
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/hard_break.js b/app/assets/javascripts/behaviors/markdown/nodes/hard_break.js
deleted file mode 100644
index 1668af9c3f4..00000000000
--- a/app/assets/javascripts/behaviors/markdown/nodes/hard_break.js
+++ /dev/null
@@ -1,14 +0,0 @@
-// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
-export default () => ({
- name: 'hard_break',
- schema: {
- inline: true,
- group: 'inline',
- selectable: false,
- parseDOM: [{ tag: 'br' }],
- toDOM: () => ['br'],
- },
- toMarkdown(state) {
- if (!state.atBlank()) state.write(' \n');
- },
-});
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/heading.js b/app/assets/javascripts/behaviors/markdown/nodes/heading.js
deleted file mode 100644
index 21b4ec69b70..00000000000
--- a/app/assets/javascripts/behaviors/markdown/nodes/heading.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import { defaultMarkdownSerializer } from '~/lib/prosemirror_markdown_serializer';
-
-// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
-export default ({ levels = [1, 2, 3, 4, 5, 6] } = {}) => ({
- name: 'heading',
- schema: {
- attrs: {
- level: {
- default: 1,
- },
- },
- content: 'inline*',
- group: 'block',
- defining: true,
- draggable: false,
- parseDOM: levels.map((level) => ({
- tag: `h${level}`,
- attrs: { level },
- })),
- toDOM: (node) => [`h${node.attrs.level}`, 0],
- },
- toMarkdown(state, node) {
- if (!node.childCount) return;
-
- defaultMarkdownSerializer.nodes.heading(state, node);
- },
-});
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/horizontal_rule.js b/app/assets/javascripts/behaviors/markdown/nodes/horizontal_rule.js
deleted file mode 100644
index 2d7074e567f..00000000000
--- a/app/assets/javascripts/behaviors/markdown/nodes/horizontal_rule.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import { defaultMarkdownSerializer } from '~/lib/prosemirror_markdown_serializer';
-
-// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
-export default () => ({
- name: 'horizontal_rule',
- schema: {
- group: 'block',
- parseDOM: [{ tag: 'hr' }],
- toDOM: () => ['hr'],
- },
- toMarkdown(state, node) {
- defaultMarkdownSerializer.nodes.horizontal_rule(state, node);
- },
-});
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/image.js b/app/assets/javascripts/behaviors/markdown/nodes/image.js
deleted file mode 100644
index 370cc347a05..00000000000
--- a/app/assets/javascripts/behaviors/markdown/nodes/image.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import { placeholderImage } from '~/lazy_loader';
-import { defaultMarkdownSerializer } from '~/lib/prosemirror_markdown_serializer';
-import { HIGHER_PARSE_RULE_PRIORITY } from '../constants';
-
-export default () => ({
- name: 'image',
- schema: {
- attrs: {
- src: {},
- alt: {
- default: null,
- },
- title: {
- default: null,
- },
- },
- group: 'inline',
- inline: true,
- draggable: true,
- parseDOM: [
- // Matches HTML generated by Banzai::Filter::ImageLinkFilter
- {
- tag: 'a.no-attachment-icon',
- priority: HIGHER_PARSE_RULE_PRIORITY,
- skip: true,
- },
- // Matches HTML generated by Banzai::Filter::ImageLazyLoadFilter
- {
- tag: 'img[src]:not(.emoji)',
- getAttrs: (el) => {
- const imageSrc = el.src;
- const imageUrl =
- imageSrc && imageSrc !== placeholderImage ? imageSrc : el.dataset.src || '';
-
- return {
- src: imageUrl,
- title: el.getAttribute('title'),
- alt: el.getAttribute('alt'),
- };
- },
- },
- ],
- toDOM: (node) => ['img', node.attrs],
- },
- toMarkdown(state, node) {
- defaultMarkdownSerializer.nodes.image(state, node);
- },
-});
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/list_item.js b/app/assets/javascripts/behaviors/markdown/nodes/list_item.js
deleted file mode 100644
index 97c1f07427d..00000000000
--- a/app/assets/javascripts/behaviors/markdown/nodes/list_item.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import { defaultMarkdownSerializer } from '~/lib/prosemirror_markdown_serializer';
-
-// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
-export default () => ({
- name: 'list_item',
- schema: {
- content: 'paragraph block*',
- defining: true,
- draggable: false,
- parseDOM: [{ tag: 'li' }],
- toDOM: () => ['li', 0],
- },
- toMarkdown(state, node) {
- defaultMarkdownSerializer.nodes.list_item(state, node);
- },
-});
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/ordered_list.js b/app/assets/javascripts/behaviors/markdown/nodes/ordered_list.js
deleted file mode 100644
index f2f3eff266a..00000000000
--- a/app/assets/javascripts/behaviors/markdown/nodes/ordered_list.js
+++ /dev/null
@@ -1,25 +0,0 @@
-// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
-export default () => ({
- name: 'ordered_list',
- schema: {
- attrs: {
- order: {
- default: 1,
- },
- },
- content: 'list_item+',
- group: 'block',
- parseDOM: [
- {
- tag: 'ol',
- getAttrs: (dom) => ({
- order: dom.hasAttribute('start') ? dom.getAttribute('start') + 1 : 1,
- }),
- },
- ],
- toDOM: (node) => (node.attrs.order === 1 ? ['ol', 0] : ['ol', { start: node.attrs.order }, 0]),
- },
- toMarkdown(state, node) {
- state.renderList(node, ' ', () => '1. ');
- },
-});
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/ordered_task_list.js b/app/assets/javascripts/behaviors/markdown/nodes/ordered_task_list.js
deleted file mode 100644
index 53a6a0d9e07..00000000000
--- a/app/assets/javascripts/behaviors/markdown/nodes/ordered_task_list.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import { HIGHER_PARSE_RULE_PRIORITY } from '../constants';
-
-// Transforms generated HTML back to GFM for Banzai::Filter::TaskListFilter
-export default () => ({
- name: 'ordered_task_list',
- schema: {
- group: 'block',
- content: '(task_list_item|list_item)+',
- parseDOM: [
- {
- priority: HIGHER_PARSE_RULE_PRIORITY,
- tag: 'ol.task-list',
- },
- ],
- toDOM: () => ['ol', { class: 'task-list' }, 0],
- },
-
- toMarkdown(state, node) {
- state.renderList(node, ' ', () => '1. ');
- },
-});
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/paragraph.js b/app/assets/javascripts/behaviors/markdown/nodes/paragraph.js
deleted file mode 100644
index 310feebb390..00000000000
--- a/app/assets/javascripts/behaviors/markdown/nodes/paragraph.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import { defaultMarkdownSerializer } from '~/lib/prosemirror_markdown_serializer';
-
-// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
-export default () => ({
- name: 'paragraph',
- schema: {
- content: 'inline*',
- group: 'block',
- parseDOM: [{ tag: 'p' }],
- toDOM: () => ['p', 0],
- },
- toMarkdown(state, node) {
- defaultMarkdownSerializer.nodes.paragraph(state, node);
- },
-});
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/playable.js b/app/assets/javascripts/behaviors/markdown/nodes/playable.js
deleted file mode 100644
index 7559c2a6a8a..00000000000
--- a/app/assets/javascripts/behaviors/markdown/nodes/playable.js
+++ /dev/null
@@ -1,56 +0,0 @@
-import { defaultMarkdownSerializer } from '~/lib/prosemirror_markdown_serializer';
-
-/**
- * Abstract base class for playable media, like video and audio.
- * Must not be instantiated directly. Subclasses must set
- * the `mediaType` property in their constructors.
- * @abstract
- */
-export default ({ mediaType, extraElementAttrs = {} }) => {
- const attrs = {
- src: {},
- alt: {
- default: null,
- },
- };
- const parseDOM = [
- {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- tag: `.${mediaType}-container`,
- getAttrs: (el) => ({
- src: el.querySelector(mediaType).src,
- alt: el.querySelector(mediaType).dataset.title,
- }),
- },
- ];
- const toDOM = (node) => [
- 'span',
- { class: `media-container ${mediaType}-container` },
- [
- mediaType,
- {
- src: node.attrs.src,
- controls: true,
- 'data-setup': '{}',
- 'data-title': node.attrs.alt,
- ...extraElementAttrs,
- },
- ],
- ['a', { href: node.attrs.src }, node.attrs.alt],
- ];
-
- return {
- name: mediaType,
- schema: {
- attrs,
- group: 'inline',
- inline: true,
- draggable: true,
- parseDOM,
- toDOM,
- },
- toMarkdown(state, node) {
- defaultMarkdownSerializer.nodes.image(state, node);
- },
- };
-};
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/reference.js b/app/assets/javascripts/behaviors/markdown/nodes/reference.js
deleted file mode 100644
index 9ae6ab07004..00000000000
--- a/app/assets/javascripts/behaviors/markdown/nodes/reference.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import { HIGHER_PARSE_RULE_PRIORITY } from '../constants';
-
-// Transforms generated HTML back to GFM for Banzai::Filter::ReferenceFilter and subclasses
-export default () => ({
- name: 'reference',
- schema: {
- inline: true,
- group: 'inline',
- atom: true,
- attrs: {
- className: {},
- referenceType: {},
- originalText: { default: null },
- href: {},
- text: {},
- },
- parseDOM: [
- {
- tag: 'a.gfm:not([data-link=true])',
- priority: HIGHER_PARSE_RULE_PRIORITY,
- getAttrs: (el) => ({
- className: el.className,
- referenceType: el.dataset.referenceType,
- originalText: el.dataset.original,
- href: el.getAttribute('href'),
- text: el.textContent,
- }),
- },
- ],
- toDOM: (node) => [
- 'a',
- {
- class: node.attrs.className,
- href: node.attrs.href,
- 'data-reference-type': node.attrs.referenceType,
- 'data-original': node.attrs.originalText,
- },
- node.attrs.text,
- ],
- },
- toMarkdown(state, node) {
- state.write(node.attrs.originalText || node.attrs.text);
- },
-});
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/summary.js b/app/assets/javascripts/behaviors/markdown/nodes/summary.js
deleted file mode 100644
index eb91b3c981e..00000000000
--- a/app/assets/javascripts/behaviors/markdown/nodes/summary.js
+++ /dev/null
@@ -1,17 +0,0 @@
-// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
-export default () => ({
- name: 'summary',
- schema: {
- content: 'text*',
- marks: '',
- defining: true,
- parseDOM: [{ tag: 'summary' }],
- toDOM: () => ['summary', 0],
- },
- toMarkdown(state, node) {
- state.write('<summary>');
- state.text(node.textContent, false);
- state.write('</summary>');
- state.closeBlock(node);
- },
-});
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table.js b/app/assets/javascripts/behaviors/markdown/nodes/table.js
deleted file mode 100644
index c766f7f1fba..00000000000
--- a/app/assets/javascripts/behaviors/markdown/nodes/table.js
+++ /dev/null
@@ -1,15 +0,0 @@
-// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
-export default () => ({
- name: 'table',
- schema: {
- content: 'table_head table_body',
- group: 'block',
- isolating: true,
- parseDOM: [{ tag: 'table' }],
- toDOM: () => ['table', 0],
- },
- toMarkdown(state, node) {
- state.renderContent(node);
- state.closeBlock(node);
- },
-});
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table_body.js b/app/assets/javascripts/behaviors/markdown/nodes/table_body.js
deleted file mode 100644
index 0a49fb558ae..00000000000
--- a/app/assets/javascripts/behaviors/markdown/nodes/table_body.js
+++ /dev/null
@@ -1,14 +0,0 @@
-// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
-export default () => ({
- name: 'table_body',
- schema: {
- content: 'table_row+',
- parseDOM: [{ tag: 'tbody' }],
- toDOM: () => ['tbody', 0],
- },
- toMarkdown: (state, node) => {
- state.flushClose(1);
- state.renderContent(node);
- state.closeBlock(node);
- },
-});
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table_cell.js b/app/assets/javascripts/behaviors/markdown/nodes/table_cell.js
deleted file mode 100644
index f46344ba43c..00000000000
--- a/app/assets/javascripts/behaviors/markdown/nodes/table_cell.js
+++ /dev/null
@@ -1,25 +0,0 @@
-// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
-export default () => ({
- name: 'table_cell',
- schema: {
- attrs: {
- header: { default: false },
- align: { default: null },
- },
- content: 'inline*',
- isolating: true,
- parseDOM: [
- {
- tag: 'td, th',
- getAttrs: (el) => ({
- header: el.tagName === 'TH',
- align: el.getAttribute('align') || el.style.textAlign,
- }),
- },
- ],
- toDOM: (node) => [node.attrs.header ? 'th' : 'td', { align: node.attrs.align }, 0],
- },
- toMarkdown: (state, node) => {
- state.renderInline(node);
- },
-});
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table_head.js b/app/assets/javascripts/behaviors/markdown/nodes/table_head.js
deleted file mode 100644
index 2e9b53ee0ac..00000000000
--- a/app/assets/javascripts/behaviors/markdown/nodes/table_head.js
+++ /dev/null
@@ -1,14 +0,0 @@
-// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
-export default () => ({
- name: 'table_head',
- schema: {
- content: 'table_header_row',
- parseDOM: [{ tag: 'thead' }],
- toDOM: () => ['thead', 0],
- },
- toMarkdown: (state, node) => {
- state.flushClose(1);
- state.renderContent(node);
- state.closeBlock(node);
- },
-});
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table_header_row.js b/app/assets/javascripts/behaviors/markdown/nodes/table_header_row.js
deleted file mode 100644
index d8aa497066c..00000000000
--- a/app/assets/javascripts/behaviors/markdown/nodes/table_header_row.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import { HIGHER_PARSE_RULE_PRIORITY } from '../constants';
-import TableRow from './table_row';
-
-const CENTER_ALIGN = 'center';
-
-// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
-export default () => ({
- name: 'table_header_row',
- schema: {
- content: 'table_cell+',
- parseDOM: [
- {
- tag: 'thead tr',
- priority: HIGHER_PARSE_RULE_PRIORITY,
- },
- ],
- toDOM: () => ['tr', 0],
- },
- toMarkdown: (state, node) => {
- const cellWidths = TableRow().toMarkdown(state, node);
-
- state.flushClose(1);
-
- state.write('|');
- node.forEach((cell, _, i) => {
- if (i) state.write('|');
-
- state.write(cell.attrs.align === CENTER_ALIGN ? ':' : '-');
- state.write(state.repeat('-', cellWidths[i]));
- state.write(cell.attrs.align === CENTER_ALIGN || cell.attrs.align === 'right' ? ':' : '-');
- });
- state.write('|');
-
- state.closeBlock(node);
- },
-});
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js b/app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js
deleted file mode 100644
index 4a0256c4644..00000000000
--- a/app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import { __ } from '~/locale';
-import { HIGHER_PARSE_RULE_PRIORITY } from '../constants';
-
-// Transforms generated HTML back to GFM for Banzai::Filter::TableOfContentsFilter
-export default () => ({
- name: 'table_of_contents',
- schema: {
- group: 'block',
- atom: true,
- parseDOM: [
- {
- tag: 'ul.section-nav',
- priority: HIGHER_PARSE_RULE_PRIORITY,
- },
- {
- tag: 'p.table-of-contents',
- priority: HIGHER_PARSE_RULE_PRIORITY,
- },
- ],
- toDOM: () => ['p', { class: 'table-of-contents' }, __('Table of Contents')],
- },
- toMarkdown: (state, node) => {
- state.write('[[_TOC_]]');
- state.closeBlock(node);
- },
-});
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table_row.js b/app/assets/javascripts/behaviors/markdown/nodes/table_row.js
deleted file mode 100644
index 3830dae4f0d..00000000000
--- a/app/assets/javascripts/behaviors/markdown/nodes/table_row.js
+++ /dev/null
@@ -1,28 +0,0 @@
-// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
-export default () => ({
- name: 'table_row',
- schema: {
- content: 'table_cell+',
- parseDOM: [{ tag: 'tr' }],
- toDOM: () => ['tr', 0],
- },
- toMarkdown: (state, node) => {
- const cellWidths = [];
-
- state.flushClose(1);
-
- state.write('| ');
- node.forEach((cell, _, i) => {
- if (i) state.write(' | ');
-
- const { length } = state.out;
- state.render(cell, node, i);
- cellWidths.push(state.out.length - length);
- });
- state.write(' |');
-
- state.closeBlock(node);
-
- return cellWidths;
- },
-});
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/task_list.js b/app/assets/javascripts/behaviors/markdown/nodes/task_list.js
deleted file mode 100644
index 3c3812ad8f7..00000000000
--- a/app/assets/javascripts/behaviors/markdown/nodes/task_list.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import { HIGHER_PARSE_RULE_PRIORITY } from '../constants';
-
-// Transforms generated HTML back to GFM for Banzai::Filter::TaskListFilter
-export default () => ({
- name: 'task_list',
- schema: {
- group: 'block',
- content: '(task_list_item|list_item)+',
- parseDOM: [
- {
- priority: HIGHER_PARSE_RULE_PRIORITY,
- tag: 'ul.task-list',
- },
- ],
- toDOM: () => ['ul', { class: 'task-list' }, 0],
- },
- toMarkdown(state, node) {
- state.renderList(node, ' ', () => '* ');
- },
-});
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js b/app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js
deleted file mode 100644
index e1c1bd58ee2..00000000000
--- a/app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js
+++ /dev/null
@@ -1,71 +0,0 @@
-import { HIGHER_PARSE_RULE_PRIORITY } from '../constants';
-
-// Transforms generated HTML back to GFM for Banzai::Filter::TaskListFilter
-export default () => ({
- name: 'task_list_item',
- schema: {
- attrs: {
- state: {
- default: null,
- },
- },
- defining: true,
- draggable: false,
- content: 'paragraph block*',
- parseDOM: [
- {
- priority: HIGHER_PARSE_RULE_PRIORITY,
- tag: 'li.task-list-item',
- getAttrs: (el) => {
- const checkbox = el.querySelector('input[type=checkbox].task-list-item-checkbox');
- if (checkbox?.matches('[data-inapplicable]')) {
- return { state: 'inapplicable' };
- }
- if (checkbox?.checked) {
- return { state: 'done' };
- }
-
- return {};
- },
- },
- ],
- toDOM(node) {
- return [
- 'li',
- {
- class: () => {
- if (node.attrs.state === 'inapplicable') {
- return 'task-list-item inapplicable';
- }
-
- return 'task-list-item';
- },
- },
- [
- 'input',
- {
- type: 'checkbox',
- class: 'task-list-item-checkbox',
- checked: node.attrs.state === 'done',
- 'data-inapplicable': node.attrs.state === 'inapplicable',
- },
- ],
- ['div', { class: 'todo-content' }, 0],
- ];
- },
- },
- toMarkdown(state, node) {
- switch (node.attrs.state) {
- case 'done':
- state.write('[x] ');
- break;
- case 'inapplicable':
- state.write('[~] ');
- break;
- default:
- state.write('[ ] ');
- break;
- }
- state.renderContent(node);
- },
-});
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/text.js b/app/assets/javascripts/behaviors/markdown/nodes/text.js
deleted file mode 100644
index 0e1f0bc0e40..00000000000
--- a/app/assets/javascripts/behaviors/markdown/nodes/text.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import { defaultMarkdownSerializer } from '~/lib/prosemirror_markdown_serializer';
-
-export default () => ({
- name: 'text',
- schema: {
- group: 'inline',
- },
- toMarkdown(state, node) {
- defaultMarkdownSerializer.nodes.text(state, node);
- },
-});
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/video.js b/app/assets/javascripts/behaviors/markdown/nodes/video.js
deleted file mode 100644
index aa1088826da..00000000000
--- a/app/assets/javascripts/behaviors/markdown/nodes/video.js
+++ /dev/null
@@ -1,4 +0,0 @@
-import playable from './playable';
-
-// Transforms generated HTML back to GFM for Banzai::Filter::VideoLinkFilter
-export default () => playable({ mediaType: 'video', extraElementAttrs: { width: '400' } });
diff --git a/app/assets/javascripts/behaviors/markdown/schema.js b/app/assets/javascripts/behaviors/markdown/schema.js
index 31bab23c8b0..baccddfc7cd 100644
--- a/app/assets/javascripts/behaviors/markdown/schema.js
+++ b/app/assets/javascripts/behaviors/markdown/schema.js
@@ -1,20 +1,13 @@
+import { flatMap } from 'lodash';
+import { Editor } from '@tiptap/vue-2';
+import OrderedMap from 'orderedmap';
import { Schema } from '@tiptap/pm/model';
-import editorExtensions from './editor_extensions';
+import * as extensions from '~/content_editor/extensions';
-const nodes = editorExtensions.nodes.reduce(
- (ns, { name, schema }) => ({
- ...ns,
- [name]: schema,
- }),
- {},
-);
+const { schema } = new Editor({ extensions: flatMap(extensions) });
-const marks = editorExtensions.marks.reduce(
- (ms, { name, schema }) => ({
- ...ms,
- [name]: schema,
- }),
- {},
-);
+const schemaSpec = { ...schema.spec };
+schemaSpec.marks = OrderedMap.from(schemaSpec.marks).remove('span');
+schemaSpec.nodes = OrderedMap.from(schemaSpec.nodes).remove('div').remove('pre');
-export default new Schema({ nodes, marks });
+export default new Schema(schemaSpec);
diff --git a/app/assets/javascripts/behaviors/markdown/serializer.js b/app/assets/javascripts/behaviors/markdown/serializer.js
index e3e8a380cd5..fa21bd4630d 100644
--- a/app/assets/javascripts/behaviors/markdown/serializer.js
+++ b/app/assets/javascripts/behaviors/markdown/serializer.js
@@ -1,20 +1,3 @@
-import { MarkdownSerializer } from '~/lib/prosemirror_markdown_serializer';
-import editorExtensions from './editor_extensions';
+import MarkdownSerializer from '~/content_editor/services/markdown_serializer';
-const nodes = editorExtensions.nodes.reduce(
- (ns, { name, toMarkdown }) => ({
- ...ns,
- [name]: toMarkdown,
- }),
- {},
-);
-
-const marks = editorExtensions.marks.reduce(
- (ms, { name, toMarkdown }) => ({
- ...ms,
- [name]: toMarkdown,
- }),
- {},
-);
-
-export default new MarkdownSerializer(nodes, marks);
+export default new MarkdownSerializer();
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 da5ac7eb158..e75178789da 100644
--- a/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
+++ b/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
@@ -65,6 +65,48 @@ export default CodeBlockLowlight.extend({
tag: 'pre.js-syntax-highlight',
preserveWhitespace: 'full',
},
+ // Matches HTML generated by Banzai::Filter::SyntaxHighlightFilter,
+ // Banzai::Filter::MathFilter, Banzai::Filter::MermaidFilter,
+ // or Banzai::Filter::SuggestionFilter
+ {
+ tag: 'pre.code.highlight',
+ preserveWhitespace: 'full',
+ },
+ // Matches HTML generated by Banzai::Filter::MathFilter,
+ // after being transformed by ~/behaviors/markdown/render_math.js
+ {
+ tag: 'span.katex-display',
+ preserveWhitespace: 'full',
+ contentElement: 'annotation[encoding="application/x-tex"]',
+ attrs: { language: 'math' },
+ },
+ // Matches HTML generated by Banzai::Filter::MermaidFilter,
+ // after being transformed by ~/behaviors/markdown/render_sandboxed_mermaid.js
+ {
+ tag: 'svg.mermaid',
+ preserveWhitespace: 'full',
+ contentElement: 'text.source',
+ attrs: { language: 'mermaid' },
+ },
+ // Matches HTML generated by Banzai::Filter::SuggestionFilter,
+ // after being transformed by ~/vue_shared/components/markdown/suggestions.vue
+ {
+ tag: '.md-suggestion',
+ skip: true,
+ },
+ {
+ tag: '.md-suggestion-header',
+ ignore: true,
+ },
+ {
+ tag: '.md-suggestion-diff',
+ preserveWhitespace: 'full',
+ getContent: (el, schema) =>
+ [...el.querySelectorAll('.line_content.new span')].map((span) =>
+ schema.text(span.innerText),
+ ),
+ attrs: { language: 'suggestion' },
+ },
];
},
renderHTML({ HTMLAttributes }) {
diff --git a/app/assets/javascripts/content_editor/extensions/code_suggestion.js b/app/assets/javascripts/content_editor/extensions/code_suggestion.js
index c70a96769fb..45090d57559 100644
--- a/app/assets/javascripts/content_editor/extensions/code_suggestion.js
+++ b/app/assets/javascripts/content_editor/extensions/code_suggestion.js
@@ -14,7 +14,7 @@ export default CodeBlockHighlight.extend({
addOptions() {
return {
lowlight,
- config: {},
+ codeSuggestionsConfig: {},
};
},
@@ -38,10 +38,13 @@ export default CodeBlockHighlight.extend({
// do not insert a new suggestion if already inside a suggestion
if (editor.isActive('codeSuggestion')) return false;
- const rawPath = ext.options.config.diffFile.view_path.replace('/blob/', '/raw/');
+ const rawPath = ext.options.codeSuggestionsConfig.diffFile.view_path.replace(
+ '/blob/',
+ '/raw/',
+ );
const allLines = (await memoizedGet(rawPath)).split('\n');
- const { line } = ext.options.config;
- let { lines } = ext.options.config;
+ const { line } = ext.options.codeSuggestionsConfig;
+ let { lines } = ext.options.codeSuggestionsConfig;
if (!lines.length) lines = [line];
diff --git a/app/assets/javascripts/content_editor/extensions/emoji.js b/app/assets/javascripts/content_editor/extensions/emoji.js
index 96e03dfe598..4cfe2c6b06c 100644
--- a/app/assets/javascripts/content_editor/extensions/emoji.js
+++ b/app/assets/javascripts/content_editor/extensions/emoji.js
@@ -1,5 +1,6 @@
import { Node, InputRule } from '@tiptap/core';
import { initEmojiMap, getEmojiMap } from '~/emoji';
+import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
export default Node.create({
name: 'emoji',
@@ -12,21 +13,10 @@ export default Node.create({
addAttributes() {
return {
- moji: {
- default: null,
- parseHTML: (element) => element.textContent,
- },
- name: {
- default: null,
- parseHTML: (element) => element.dataset.name,
- },
- title: {
- default: null,
- },
- unicodeVersion: {
- default: '6.0',
- parseHTML: (element) => element.dataset.unicodeVersion,
- },
+ moji: { default: null },
+ name: { default: null },
+ title: { default: null },
+ unicodeVersion: { default: '6.0' },
};
},
@@ -34,6 +24,26 @@ export default Node.create({
return [
{
tag: 'gl-emoji',
+ getAttrs: (el) => ({
+ name: el.dataset.name,
+ title: el.getAttribute('title'),
+ moji: el.textContent,
+ unicodeVersion: el.dataset.unicodeVersion || '6.0',
+ }),
+ },
+ {
+ tag: 'img.emoji',
+ getAttrs: (el) => {
+ const name = el.getAttribute('title').replace(/^:|:$/g, '');
+
+ return {
+ name,
+ title: name,
+ moji: name,
+ unicodeVersion: 'custom',
+ };
+ },
+ priority: PARSE_HTML_PRIORITY_HIGHEST,
},
];
},
diff --git a/app/assets/javascripts/content_editor/extensions/index.js b/app/assets/javascripts/content_editor/extensions/index.js
new file mode 100644
index 00000000000..c611d4aff07
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/index.js
@@ -0,0 +1,62 @@
+export { default as Attachment } from './attachment';
+export { default as Audio } from './audio';
+export { default as Blockquote } from './blockquote';
+export { default as Bold } from './bold';
+export { default as BulletList } from './bullet_list';
+export { default as Code } from './code';
+export { default as CodeBlockHighlight } from './code_block_highlight';
+export { default as CodeSuggestion } from './code_suggestion';
+export { default as ColorChip } from './color_chip';
+export { default as CopyPaste } from './copy_paste';
+export { default as DescriptionItem } from './description_item';
+export { default as DescriptionList } from './description_list';
+export { default as Details } from './details';
+export { default as DetailsContent } from './details_content';
+export { default as Diagram } from './diagram';
+export { default as DrawioDiagram } from './drawio_diagram';
+export { default as Document } from './document';
+export { default as Dropcursor } from './dropcursor';
+export { default as Emoji } from './emoji';
+export { default as ExternalKeydownHandler } from './external_keydown_handler';
+export { default as Figure } from './figure';
+export { default as FigureCaption } from './figure_caption';
+export { default as FootnoteDefinition } from './footnote_definition';
+export { default as FootnoteReference } from './footnote_reference';
+export { default as FootnotesSection } from './footnotes_section';
+export { default as Frontmatter } from './frontmatter';
+export { default as Gapcursor } from './gapcursor';
+export { default as HardBreak } from './hard_break';
+export { default as Heading } from './heading';
+export { default as History } from './history';
+export { default as Highlight } from './highlight';
+export { default as HorizontalRule } from './horizontal_rule';
+export { default as HTMLMarks } from './html_marks';
+export { default as HTMLNodes } from './html_nodes';
+export { default as Image } from './image';
+export { default as InlineDiff } from './inline_diff';
+export { default as Italic } from './italic';
+export { default as Link } from './link';
+export { default as ListItem } from './list_item';
+export { default as Loading } from './loading';
+export { default as MathInline } from './math_inline';
+export { default as OrderedList } from './ordered_list';
+export { default as Paragraph } from './paragraph';
+export { default as Reference } from './reference';
+export { default as ReferenceLabel } from './reference_label';
+export { default as ReferenceDefinition } from './reference_definition';
+export { default as Selection } from './selection';
+export { default as Sourcemap } from './sourcemap';
+export { default as Strike } from './strike';
+export { default as Subscript } from './subscript';
+export { default as Suggestions } from './suggestions';
+export { default as Superscript } from './superscript';
+export { default as Table } from './table';
+export { default as TableCell } from './table_cell';
+export { default as TableHeader } from './table_header';
+export { default as TableOfContents } from './table_of_contents';
+export { default as TableRow } from './table_row';
+export { default as TaskItem } from './task_item';
+export { default as TaskList } from './task_list';
+export { default as Text } from './text';
+export { default as Video } from './video';
+export { default as WordBreak } from './word_break';
diff --git a/app/assets/javascripts/content_editor/extensions/inline_diff.js b/app/assets/javascripts/content_editor/extensions/inline_diff.js
index f76943a0669..905b1f2d42e 100644
--- a/app/assets/javascripts/content_editor/extensions/inline_diff.js
+++ b/app/assets/javascripts/content_editor/extensions/inline_diff.js
@@ -13,7 +13,6 @@ export default Mark.create({
return {
type: {
default: 'addition',
- parseHTML: (element) => (element.classList.contains('deletion') ? 'deletion' : 'addition'),
},
};
},
@@ -21,7 +20,12 @@ export default Mark.create({
parseHTML() {
return [
{
- tag: 'span.idiff',
+ tag: 'span.idiff.addition',
+ attrs: { type: 'addition' },
+ },
+ {
+ tag: 'span.idiff.deletion',
+ attrs: { type: 'deletion' },
},
];
},
diff --git a/app/assets/javascripts/content_editor/extensions/math_inline.js b/app/assets/javascripts/content_editor/extensions/math_inline.js
index 4844f6feb29..b95ef10550f 100644
--- a/app/assets/javascripts/content_editor/extensions/math_inline.js
+++ b/app/assets/javascripts/content_editor/extensions/math_inline.js
@@ -1,15 +1,21 @@
import { Mark, markInputRule } from '@tiptap/core';
import { __ } from '~/locale';
-import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
+import { PARSE_HTML_PRIORITY_HIGH } from '../constants';
export default Mark.create({
name: 'mathInline',
parseHTML() {
return [
+ // Matches HTML generated by Banzai::Filter::MathFilter
{
tag: 'code.math[data-math-style=inline]',
- priority: PARSE_HTML_PRIORITY_HIGHEST,
+ priority: PARSE_HTML_PRIORITY_HIGH,
+ },
+ // Matches HTML after being transformed by app/assets/javascripts/behaviors/markdown/render_math.js
+ {
+ tag: 'span.katex',
+ contentElement: 'annotation[encoding="application/x-tex"]',
},
];
},
diff --git a/app/assets/javascripts/content_editor/extensions/strike.js b/app/assets/javascripts/content_editor/extensions/strike.js
index b6c9a968fc2..ea1abd35b87 100644
--- a/app/assets/javascripts/content_editor/extensions/strike.js
+++ b/app/assets/javascripts/content_editor/extensions/strike.js
@@ -1 +1,22 @@
-export { Strike as default } from '@tiptap/extension-strike';
+import { Strike } from '@tiptap/extension-strike';
+
+export default Strike.extend({
+ addAttributes() {
+ return {
+ ...this.parent?.(),
+
+ htmlTag: {
+ default: null,
+ renderHTML: () => '',
+ },
+ };
+ },
+
+ parseHTML() {
+ return [
+ { tag: 'del' },
+ { tag: 's', attrs: { htmlTag: 's' } },
+ { tag: 'strike', attrs: { htmlTag: 'strike' } },
+ ];
+ },
+});
diff --git a/app/assets/javascripts/content_editor/extensions/task_item.js b/app/assets/javascripts/content_editor/extensions/task_item.js
index 1e19878be9b..50ea6f393d9 100644
--- a/app/assets/javascripts/content_editor/extensions/task_item.js
+++ b/app/assets/javascripts/content_editor/extensions/task_item.js
@@ -42,7 +42,7 @@ export default TaskItem.extend({
priority: PARSE_HTML_PRIORITY_HIGHEST,
},
{
- tag: 'li.task-list-item.inapplicable s',
+ tag: 'li.inapplicable > s, li.inapplicable > p:first-of-type > s',
skip: true,
priority: PARSE_HTML_PRIORITY_HIGHEST,
},
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 5c48c0b1d43..b7082910161 100644
--- a/app/assets/javascripts/content_editor/services/create_content_editor.js
+++ b/app/assets/javascripts/content_editor/services/create_content_editor.js
@@ -1,69 +1,8 @@
import { Editor } from '@tiptap/vue-2';
-import { isFunction } from 'lodash';
+import { isFunction, flatMap } from 'lodash';
import eventHubFactory from '~/helpers/event_hub_factory';
import { PROVIDE_SERIALIZER_OR_RENDERER_ERROR } from '../constants';
-import Attachment from '../extensions/attachment';
-import Audio from '../extensions/audio';
-import Blockquote from '../extensions/blockquote';
-import Bold from '../extensions/bold';
-import BulletList from '../extensions/bullet_list';
-import Code from '../extensions/code';
-import CodeBlockHighlight from '../extensions/code_block_highlight';
-import CodeSuggestion from '../extensions/code_suggestion';
-import ColorChip from '../extensions/color_chip';
-import CopyPaste from '../extensions/copy_paste';
-import DescriptionItem from '../extensions/description_item';
-import DescriptionList from '../extensions/description_list';
-import Details from '../extensions/details';
-import DetailsContent from '../extensions/details_content';
-import Diagram from '../extensions/diagram';
-import DrawioDiagram from '../extensions/drawio_diagram';
-import Document from '../extensions/document';
-import Dropcursor from '../extensions/dropcursor';
-import Emoji from '../extensions/emoji';
-import ExternalKeydownHandler from '../extensions/external_keydown_handler';
-import Figure from '../extensions/figure';
-import FigureCaption from '../extensions/figure_caption';
-import FootnoteDefinition from '../extensions/footnote_definition';
-import FootnoteReference from '../extensions/footnote_reference';
-import FootnotesSection from '../extensions/footnotes_section';
-import Frontmatter from '../extensions/frontmatter';
-import Gapcursor from '../extensions/gapcursor';
-import HardBreak from '../extensions/hard_break';
-import Heading from '../extensions/heading';
-import History from '../extensions/history';
-import Highlight from '../extensions/highlight';
-import HorizontalRule from '../extensions/horizontal_rule';
-import HTMLMarks from '../extensions/html_marks';
-import HTMLNodes from '../extensions/html_nodes';
-import Image from '../extensions/image';
-import InlineDiff from '../extensions/inline_diff';
-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';
-import ReferenceLabel from '../extensions/reference_label';
-import ReferenceDefinition from '../extensions/reference_definition';
-import Selection from '../extensions/selection';
-import Sourcemap from '../extensions/sourcemap';
-import Strike from '../extensions/strike';
-import Subscript from '../extensions/subscript';
-import Suggestions from '../extensions/suggestions';
-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 * as builtInExtensions from '../extensions';
import { ContentEditor } from './content_editor';
import MarkdownSerializer from './markdown_serializer';
import createGlApiMarkdownDeserializer from './gl_api_markdown_deserializer';
@@ -107,68 +46,18 @@ export const createContentEditor = ({
render: renderMarkdown,
});
- const builtInContentEditorExtensions = [
- Attachment.configure({ uploadsPath, renderMarkdown, eventHub }),
- Audio,
- Blockquote,
- Bold,
- BulletList,
- Code,
- ColorChip,
- CodeBlockHighlight,
- CodeSuggestion.configure({ config: codeSuggestionsConfig }),
- DescriptionItem,
- DescriptionList,
- Details,
- DetailsContent,
- Document,
- Diagram,
- Dropcursor,
- Emoji,
- Figure,
- FigureCaption,
- FootnoteDefinition,
- FootnoteReference,
- FootnotesSection,
- Frontmatter,
- Gapcursor,
- HardBreak,
- Heading,
- History,
- Highlight,
- HorizontalRule,
- ...HTMLMarks,
- ...HTMLNodes,
- Image,
- InlineDiff,
- Italic,
- ExternalKeydownHandler.configure({ eventHub }),
- Link,
- ListItem,
- Loading,
- MathInline,
- OrderedList,
- Paragraph,
- CopyPaste.configure({ eventHub, renderMarkdown, serializer }),
- Reference.configure({ assetResolver }),
- ReferenceLabel,
- ReferenceDefinition,
- Selection,
- Sourcemap,
- Strike,
- Subscript,
- Superscript,
- TableCell,
- TableHeader,
- TableOfContents,
- TableRow,
- Table,
- TaskItem,
- TaskList,
- Text,
- Video,
- WordBreak,
- ];
+ const { Suggestions, DrawioDiagram, ...otherExtensions } = builtInExtensions;
+
+ const builtInContentEditorExtensions = flatMap(otherExtensions).map((ext) =>
+ ext.configure({
+ uploadsPath,
+ renderMarkdown,
+ eventHub,
+ codeSuggestionsConfig,
+ serializer,
+ assetResolver,
+ }),
+ );
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 3b759de57f2..5688f30dcc3 100644
--- a/app/assets/javascripts/content_editor/services/markdown_serializer.js
+++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js
@@ -2,56 +2,7 @@ import {
MarkdownSerializer as ProseMirrorMarkdownSerializer,
defaultMarkdownSerializer,
} from '~/lib/prosemirror_markdown_serializer';
-import Audio from '../extensions/audio';
-import Blockquote from '../extensions/blockquote';
-import Bold from '../extensions/bold';
-import BulletList from '../extensions/bullet_list';
-import Code from '../extensions/code';
-import CodeBlockHighlight from '../extensions/code_block_highlight';
-import CodeSuggestion from '../extensions/code_suggestion';
-import DescriptionItem from '../extensions/description_item';
-import DescriptionList from '../extensions/description_list';
-import Details from '../extensions/details';
-import DetailsContent from '../extensions/details_content';
-import DrawioDiagram from '../extensions/drawio_diagram';
-import Diagram from '../extensions/diagram';
-import Emoji from '../extensions/emoji';
-import Figure from '../extensions/figure';
-import FigureCaption from '../extensions/figure_caption';
-import FootnoteDefinition from '../extensions/footnote_definition';
-import FootnoteReference from '../extensions/footnote_reference';
-import Frontmatter from '../extensions/frontmatter';
-import HardBreak from '../extensions/hard_break';
-import Heading from '../extensions/heading';
-import HorizontalRule from '../extensions/horizontal_rule';
-import Highlight from '../extensions/highlight';
-import HTMLMarks from '../extensions/html_marks';
-import HTMLNodes from '../extensions/html_nodes';
-import Image from '../extensions/image';
-import InlineDiff from '../extensions/inline_diff';
-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';
-import ReferenceLabel from '../extensions/reference_label';
-import ReferenceDefinition from '../extensions/reference_definition';
-import Strike from '../extensions/strike';
-import Subscript from '../extensions/subscript';
-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 * as extensions from '../extensions';
import {
renderCodeBlock,
renderHardBreak,
@@ -62,6 +13,8 @@ import {
closeTag,
renderOrderedList,
renderImage,
+ renderHeading,
+ renderBlockquote,
renderPlayable,
renderHTMLNode,
renderContent,
@@ -78,13 +31,13 @@ import {
const defaultSerializerConfig = {
marks: {
- [Bold.name]: bold,
- [Italic.name]: italic,
- [Code.name]: code,
- [Subscript.name]: { open: '<sub>', close: '</sub>', mixable: true },
- [Superscript.name]: { open: '<sup>', close: '</sup>', mixable: true },
- [Highlight.name]: { open: '<mark>', close: '</mark>', mixable: true },
- [InlineDiff.name]: {
+ [extensions.Bold.name]: bold,
+ [extensions.Italic.name]: italic,
+ [extensions.Code.name]: code,
+ [extensions.Subscript.name]: { open: '<sub>', close: '</sub>', mixable: true },
+ [extensions.Superscript.name]: { open: '<sup>', close: '</sup>', mixable: true },
+ [extensions.Highlight.name]: { open: '<mark>', close: '</mark>', mixable: true },
+ [extensions.InlineDiff.name]: {
mixable: true,
open(_, mark) {
return mark.attrs.type === 'addition' ? '{+' : '{-';
@@ -93,14 +46,14 @@ const defaultSerializerConfig = {
return mark.attrs.type === 'addition' ? '+}' : '-}';
},
},
- [Link.name]: link,
- [MathInline.name]: {
+ [extensions.Link.name]: link,
+ [extensions.MathInline.name]: {
open: (...args) => `$${defaultMarkdownSerializer.marks.code.open(...args)}`,
close: (...args) => `${defaultMarkdownSerializer.marks.code.close(...args)}$`,
escape: false,
},
- [Strike.name]: strike,
- ...HTMLMarks.reduce(
+ [extensions.Strike.name]: strike,
+ ...extensions.HTMLMarks.reduce(
(acc, { name }) => ({
...acc,
[name]: {
@@ -116,38 +69,27 @@ const defaultSerializerConfig = {
},
nodes: {
- [Audio.name]: preserveUnchanged({
+ [extensions.Audio.name]: preserveUnchanged({
render: renderPlayable,
inline: true,
}),
- [Blockquote.name]: preserveUnchanged((state, node) => {
- if (node.attrs.multiline) {
- state.write('>>>');
- state.ensureNewLine();
- state.renderContent(node);
- state.ensureNewLine();
- state.write('>>>');
- state.closeBlock(node);
- } else {
- state.wrapBlock('> ', null, node, () => state.renderContent(node));
- }
- }),
- [BulletList.name]: preserveUnchanged(renderBulletList),
- [CodeBlockHighlight.name]: preserveUnchanged(renderCodeBlock),
- [Diagram.name]: preserveUnchanged(renderCodeBlock),
- [CodeSuggestion.name]: preserveUnchanged(renderCodeBlock),
- [DrawioDiagram.name]: preserveUnchanged({
+ [extensions.Blockquote.name]: preserveUnchanged(renderBlockquote),
+ [extensions.BulletList.name]: preserveUnchanged(renderBulletList),
+ [extensions.CodeBlockHighlight.name]: preserveUnchanged(renderCodeBlock),
+ [extensions.Diagram.name]: preserveUnchanged(renderCodeBlock),
+ [extensions.CodeSuggestion.name]: preserveUnchanged(renderCodeBlock),
+ [extensions.DrawioDiagram.name]: preserveUnchanged({
render: renderImage,
inline: true,
}),
- [DescriptionList.name]: renderHTMLNode('dl', true),
- [DescriptionItem.name]: (state, node, parent, index) => {
+ [extensions.DescriptionList.name]: renderHTMLNode('dl', true),
+ [extensions.DescriptionItem.name]: (state, node, parent, index) => {
if (index === 1) state.ensureNewLine();
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) => {
+ [extensions.Details.name]: renderHTMLNode('details', true),
+ [extensions.DetailsContent.name]: (state, node, parent, index) => {
if (!index) renderHTMLNode('summary')(state, node);
else {
if (index === 1) state.ensureNewLine();
@@ -155,23 +97,23 @@ const defaultSerializerConfig = {
if (index === parent.childCount - 1) state.ensureNewLine();
}
},
- [Emoji.name]: (state, node) => {
+ [extensions.Emoji.name]: (state, node) => {
const { name } = node.attrs;
state.write(`:${name}:`);
},
- [FootnoteDefinition.name]: preserveUnchanged((state, node) => {
+ [extensions.FootnoteDefinition.name]: preserveUnchanged((state, node) => {
state.write(`[^${node.attrs.identifier}]: `);
state.renderInline(node);
state.ensureNewLine();
}),
- [FootnoteReference.name]: preserveUnchanged({
+ [extensions.FootnoteReference.name]: preserveUnchanged({
render: (state, node) => {
state.write(`[^${node.attrs.identifier}]`);
},
inline: true,
}),
- [Frontmatter.name]: preserveUnchanged((state, node) => {
+ [extensions.Frontmatter.name]: preserveUnchanged((state, node) => {
const { language } = node.attrs;
const syntax = {
toml: '+++',
@@ -185,22 +127,24 @@ const defaultSerializerConfig = {
state.write(syntax);
state.closeBlock(node);
}),
- [Figure.name]: renderHTMLNode('figure'),
- [FigureCaption.name]: renderHTMLNode('figcaption'),
- [HardBreak.name]: preserveUnchanged(renderHardBreak),
- [Heading.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.heading),
- [HorizontalRule.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.horizontal_rule),
- [Image.name]: preserveUnchanged({
+ [extensions.Figure.name]: renderHTMLNode('figure'),
+ [extensions.FigureCaption.name]: renderHTMLNode('figcaption'),
+ [extensions.HardBreak.name]: preserveUnchanged(renderHardBreak),
+ [extensions.Heading.name]: preserveUnchanged(renderHeading),
+ [extensions.HorizontalRule.name]: preserveUnchanged(
+ defaultMarkdownSerializer.nodes.horizontal_rule,
+ ),
+ [extensions.Image.name]: preserveUnchanged({
render: renderImage,
inline: true,
}),
- [ListItem.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.list_item),
- [Loading.name]: () => {},
- [OrderedList.name]: preserveUnchanged(renderOrderedList),
- [Paragraph.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.paragraph),
- [Reference.name]: renderReference,
- [ReferenceLabel.name]: renderReferenceLabel,
- [ReferenceDefinition.name]: preserveUnchanged({
+ [extensions.ListItem.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.list_item),
+ [extensions.Loading.name]: () => {},
+ [extensions.OrderedList.name]: preserveUnchanged(renderOrderedList),
+ [extensions.Paragraph.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.paragraph),
+ [extensions.Reference.name]: renderReference,
+ [extensions.ReferenceLabel.name]: renderReferenceLabel,
+ [extensions.ReferenceDefinition.name]: preserveUnchanged({
render: (state, node, parent, index, same, sourceMarkdown) => {
const nextSibling = parent.maybeChild(index + 1);
@@ -211,7 +155,7 @@ const defaultSerializerConfig = {
* because it isn’t necessary and a more compact text format
* is preferred.
*/
- if (!nextSibling || nextSibling.type.name !== ReferenceDefinition.name) {
+ if (!nextSibling || nextSibling.type.name !== extensions.ReferenceDefinition.name) {
state.closeBlock(node);
} else {
state.ensureNewLine();
@@ -219,34 +163,35 @@ const defaultSerializerConfig = {
},
overwriteSourcePreservationStrategy: true,
}),
- [TableOfContents.name]: preserveUnchanged((state, node) => {
+ [extensions.TableOfContents.name]: preserveUnchanged((state, node) => {
state.write('[[_TOC_]]');
state.closeBlock(node);
}),
- [Table.name]: preserveUnchanged(renderTable),
- [TableCell.name]: renderTableCell,
- [TableHeader.name]: renderTableCell,
- [TableRow.name]: renderTableRow,
- [TaskItem.name]: preserveUnchanged((state, node) => {
+ [extensions.Table.name]: preserveUnchanged(renderTable),
+ [extensions.TableCell.name]: renderTableCell,
+ [extensions.TableHeader.name]: renderTableCell,
+ [extensions.TableRow.name]: renderTableRow,
+ [extensions.TaskItem.name]: preserveUnchanged((state, node) => {
let symbol = ' ';
if (node.attrs.inapplicable) symbol = '~';
else if (node.attrs.checked) symbol = 'x';
state.write(`[${symbol}] `);
+
if (!node.textContent) state.write('&nbsp;');
state.renderContent(node);
}),
- [TaskList.name]: preserveUnchanged((state, node) => {
+ [extensions.TaskList.name]: preserveUnchanged((state, node) => {
if (node.attrs.numeric) renderOrderedList(state, node);
else renderBulletList(state, node);
}),
- [Text.name]: defaultMarkdownSerializer.nodes.text,
- [Video.name]: preserveUnchanged({
+ [extensions.Text.name]: defaultMarkdownSerializer.nodes.text,
+ [extensions.Video.name]: preserveUnchanged({
render: renderPlayable,
inline: true,
}),
- [WordBreak.name]: (state) => state.write('<wbr>'),
- ...HTMLNodes.reduce((serializers, htmlNode) => {
+ [extensions.WordBreak.name]: (state) => state.write('<wbr>'),
+ ...extensions.HTMLNodes.reduce((serializers, htmlNode) => {
return {
...serializers,
[htmlNode.name]: (state, node) => renderHTMLNode(htmlNode.options.tagName)(state, node),
@@ -310,7 +255,7 @@ export default class MarkdownSerializer {
* changed.
* @returns A String that represents the serialized document as Markdown
*/
- serialize({ doc, pristineDoc }) {
+ serialize({ doc, pristineDoc }, { useCanonicalSrc = true, skipEmptyNodes = false } = {}) {
const changeTracker = createChangeTracker(doc, pristineDoc);
const serializer = new ProseMirrorMarkdownSerializer(
{
@@ -325,6 +270,8 @@ export default class MarkdownSerializer {
return serializer.serialize(doc, {
tightLists: true,
+ useCanonicalSrc,
+ skipEmptyNodes,
changeTracker,
escapeExtraCharacters: /<|>/g,
});
diff --git a/app/assets/javascripts/content_editor/services/serialization_helpers.js b/app/assets/javascripts/content_editor/services/serialization_helpers.js
index 7a2fbf8fcab..4bf61e34120 100644
--- a/app/assets/javascripts/content_editor/services/serialization_helpers.js
+++ b/app/assets/javascripts/content_editor/services/serialization_helpers.js
@@ -1,4 +1,5 @@
-import { uniq, isString, omit, isFunction } from 'lodash';
+import { uniq, omit, isFunction } from 'lodash';
+import { defaultMarkdownSerializer } from '~/lib/prosemirror_markdown_serializer';
import { removeLastSlashInUrlPath, removeUrlProtocol } from '../../lib/utils/url_utility';
const defaultAttrs = {
@@ -110,8 +111,8 @@ function htmlEncode(str = '') {
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
- .replace(/'/g, '&#39;')
- .replace(/"/g, '&#34;');
+ .replace(/'/g, '&apos;')
+ .replace(/"/g, '&quot;');
}
const shouldIgnoreAttr = (tagName, attrKey, attrValue) =>
@@ -337,15 +338,22 @@ export function renderHardBreak(state, node, parent, index) {
}
}
+function getMediaSrc(node, useCanonicalSrc = true) {
+ const { canonicalSrc, src } = node.attrs;
+
+ if (useCanonicalSrc) return canonicalSrc || src || '';
+ return src || '';
+}
+
export function renderImage(state, node) {
- const { alt, canonicalSrc, src, title, width, height, isReference } = node.attrs;
- const realSrc = canonicalSrc || src || '';
+ const { alt, title, width, height, isReference } = node.attrs;
+ const realSrc = getMediaSrc(node, state.options.useCanonicalSrc);
// eslint-disable-next-line @gitlab/require-i18n-strings
if (realSrc.startsWith('data:') || realSrc.startsWith('blob:')) return;
- if (isString(src) || isString(canonicalSrc)) {
+ if (realSrc) {
const quotedTitle = title ? ` ${state.quote(title)}` : '';
- const sourceExpression = isReference ? `[${canonicalSrc}]` : `(${realSrc}${quotedTitle})`;
+ const sourceExpression = isReference ? `[${realSrc}]` : `(${realSrc}${quotedTitle})`;
const sizeAttributes = [];
if (width) {
@@ -365,12 +373,44 @@ export function renderPlayable(state, node) {
renderImage(state, node);
}
+export function renderHeading(state, node) {
+ if (state.options.skipEmptyNodes && !node.childCount) return;
+
+ defaultMarkdownSerializer.nodes.heading(state, node);
+}
+
+export function renderBlockquote(state, node) {
+ if (state.options.skipEmptyNodes) {
+ if (!node.childCount) return;
+ if (node.childCount === 1) {
+ const child = node.child(0);
+ if (child.type.name === 'paragraph' && !child.childCount) return;
+ }
+ }
+
+ if (node.attrs.multiline) {
+ state.write('>>>');
+ state.ensureNewLine();
+ state.renderContent(node);
+ state.ensureNewLine();
+ state.write('>>>');
+ state.closeBlock(node);
+ } else {
+ state.wrapBlock('> ', null, node, () => state.renderContent(node));
+ }
+}
+
export function renderCodeBlock(state, node) {
+ if (state.options.skipEmptyNodes && !node.childCount) return;
+
+ let { language } = node.attrs;
+ if (language === 'plaintext') language = '';
+
const numBackticks = Math.max(2, node.textContent.match(/```+/g)?.[0]?.length || 0) + 1;
const backticks = state.repeat('`', numBackticks);
state.write(
`${backticks}${
- (node.attrs.language || '') + (node.attrs.langParams ? `:${node.attrs.langParams}` : '')
+ (language || '') + (node.attrs.langParams ? `:${node.attrs.langParams}` : '')
}\n`,
);
state.text(node.textContent, false);
@@ -641,13 +681,20 @@ const isAutoLink = (linkMark, parent) => {
*/
const isBracketAutoLink = (sourceMarkdown) => /^<.+?>$/.test(sourceMarkdown);
+function getLinkHref(mark, useCanonicalSrc = true) {
+ const { canonicalSrc, href } = mark.attrs;
+
+ if (useCanonicalSrc) return canonicalSrc || href || '';
+ return href || '';
+}
+
export const link = {
open(state, mark, parent) {
if (isAutoLink(mark, parent)) {
return isBracketAutoLink(mark.attrs.sourceMarkdown) ? '<' : '';
}
- const { canonicalSrc, href, title, sourceMarkdown } = mark.attrs;
+ const { href, title, sourceMarkdown } = mark.attrs;
// eslint-disable-next-line @gitlab/require-i18n-strings
if (href.startsWith('data:') || href.startsWith('blob:')) return '';
@@ -656,7 +703,9 @@ export const link = {
return '[';
}
- const attrs = { href: state.esc(href || canonicalSrc || '') };
+ const attrs = {
+ href: state.esc(getLinkHref(mark, state.options.useCanonicalSrc)),
+ };
if (title) {
attrs.title = title;
@@ -669,25 +718,29 @@ export const link = {
return isBracketAutoLink(mark.attrs.sourceMarkdown) ? '>' : '';
}
- const { canonicalSrc, href, title, sourceMarkdown, isReference } = mark.attrs;
+ const { href, title, sourceMarkdown, isReference } = mark.attrs;
// eslint-disable-next-line @gitlab/require-i18n-strings
if (href.startsWith('data:') || href.startsWith('blob:')) return '';
if (isReference) {
- return `][${state.esc(canonicalSrc || href || '')}]`;
+ return `][${state.esc(getLinkHref(mark, state.options.useCanonicalSrc))}]`;
}
if (linkType(sourceMarkdown) === LINK_HTML) {
return closeTag('a');
}
- return `](${state.esc(canonicalSrc || href || '')}${title ? ` ${state.quote(title)}` : ''})`;
+ return `](${state.esc(getLinkHref(mark, state.options.useCanonicalSrc))}${
+ title ? ` ${state.quote(title)}` : ''
+ })`;
},
};
const generateStrikeTag = (wrapTagName = openTag) => {
return (_, mark) => {
+ if (mark.attrs.htmlTag) return wrapTagName(mark.attrs.htmlTag);
+
const type = /^(~~|<del|<strike|<s).*/.exec(mark.attrs.sourceMarkdown)?.[1];
switch (type) {
diff --git a/app/models/organizations/organization_setting.rb b/app/models/organizations/organization_setting.rb
index 108531e6701..fb141c164d3 100644
--- a/app/models/organizations/organization_setting.rb
+++ b/app/models/organizations/organization_setting.rb
@@ -2,6 +2,8 @@
module Organizations
class OrganizationSetting < ApplicationRecord
+ extend ::Organization::CurrentOrganization
+
belongs_to :organization
validates :settings, json_schema: { filename: "organization_settings" }
@@ -16,5 +18,11 @@ module Organizations
end
end
end
+
+ def self.for_current_organization
+ return unless current_organization
+
+ current_organization.settings || current_organization.build_settings
+ end
end
end
diff --git a/glfm_specification/output_example_snapshots/prosemirror_json.yml b/glfm_specification/output_example_snapshots/prosemirror_json.yml
index ba2cbc8322c..34b8118cd78 100644
--- a/glfm_specification/output_example_snapshots/prosemirror_json.yml
+++ b/glfm_specification/output_example_snapshots/prosemirror_json.yml
@@ -3352,7 +3352,10 @@
"type": "text",
"marks": [
{
- "type": "strike"
+ "type": "strike",
+ "attrs": {
+ "htmlTag": null
+ }
}
],
"text": "\n*foo*\n"
@@ -3375,7 +3378,10 @@
"type": "italic"
},
{
- "type": "strike"
+ "type": "strike",
+ "attrs": {
+ "htmlTag": null
+ }
}
],
"text": "foo"
@@ -3398,7 +3404,10 @@
"type": "italic"
},
{
- "type": "strike"
+ "type": "strike",
+ "attrs": {
+ "htmlTag": null
+ }
}
],
"text": "foo"
@@ -14842,7 +14851,10 @@
"type": "text",
"marks": [
{
- "type": "strike"
+ "type": "strike",
+ "attrs": {
+ "htmlTag": null
+ }
}
],
"text": "Hi"
@@ -22303,7 +22315,10 @@
"type": "text",
"marks": [
{
- "type": "strike"
+ "type": "strike",
+ "attrs": {
+ "htmlTag": null
+ }
}
],
"text": "great"
@@ -23125,7 +23140,10 @@
"type": "text",
"marks": [
{
- "type": "strike"
+ "type": "strike",
+ "attrs": {
+ "htmlTag": null
+ }
}
],
"text": "del"
@@ -23267,7 +23285,10 @@
"type": "text",
"marks": [
{
- "type": "strike"
+ "type": "strike",
+ "attrs": {
+ "htmlTag": null
+ }
}
],
"text": "strike"
@@ -23771,7 +23792,10 @@
}
},
{
- "type": "strike"
+ "type": "strike",
+ "attrs": {
+ "htmlTag": null
+ }
}
],
"text": "www.google.com"
@@ -23795,7 +23819,10 @@
}
},
{
- "type": "strike"
+ "type": "strike",
+ "attrs": {
+ "htmlTag": null
+ }
}
],
"text": "http://google.com"
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index 64e0478734b..a42b086319d 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -24,12 +24,18 @@ module Gitlab
Gitlab::SafeRequestStore.delete(:current_application_settings)
end
+ # rubocop:disable GitlabSecurity/PublicSend -- Method calls are forwarded to one of the setting classes
def method_missing(name, *args, **kwargs, &block)
- current_application_settings.send(name, *args, **kwargs, &block) # rubocop:disable GitlabSecurity/PublicSend
+ application_settings = current_application_settings
+
+ return application_settings.send(name, *args, **kwargs, &block) if application_settings.respond_to?(name)
+
+ Organizations::OrganizationSetting.for_current_organization.send(name, *args, **kwargs, &block)
end
+ # rubocop:enable GitlabSecurity/PublicSend
def respond_to_missing?(name, include_private = false)
- current_application_settings.respond_to?(name, include_private) || super
+ current_application_settings.respond_to?(name, include_private) || Organizations::OrganizationSetting.for_current_organization.respond_to?(name, include_private) || super
end
end
end
diff --git a/lib/gitlab/fake_application_settings.rb b/lib/gitlab/fake_application_settings.rb
index 0b9a4c161ae..da99d9b1b70 100644
--- a/lib/gitlab/fake_application_settings.rb
+++ b/lib/gitlab/fake_application_settings.rb
@@ -46,6 +46,10 @@ module Gitlab
def method_missing(*)
nil
end
+
+ def respond_to_missing?(*)
+ true
+ end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index e04e50bc347..e478817381d 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -48467,9 +48467,6 @@ msgstr ""
msgid "TXT"
msgstr ""
-msgid "Table of Contents"
-msgstr ""
-
msgid "Table of contents"
msgstr ""
diff --git a/spec/features/markdown/copy_as_gfm_spec.rb b/spec/features/markdown/copy_as_gfm_spec.rb
index 8073e7e9556..0f10da6ebad 100644
--- a/spec/features/markdown/copy_as_gfm_spec.rb
+++ b/spec/features/markdown/copy_as_gfm_spec.rb
@@ -29,7 +29,7 @@ RSpec.describe 'Copy as GFM', :js, feature_category: :team_planning do
it 'transforms HTML to GFM', :aggregate_failures do
verify(
'nesting',
- '> 1. [x] **[$`2 + 2`$ {-=-}{+=+} 2^2 ~~:thumbsup:~~](http://google.com)**'
+ '> 1. [x] [**$`2 + 2`$ {-=-}{+=+} 2^2 ~~:thumbsup:~~**](http://google.com)'
)
verify(
@@ -114,9 +114,9 @@ RSpec.describe 'Copy as GFM', :js, feature_category: :team_planning do
GFM
<<~GFM,
1. [ ] Unchecked ordered task
- 1. [x] Checked ordered task
- 1. [~] Inapplicable ordered task
- 1. [~] Inapplicable ordered task with ~~del~~ and <s>strike</s> embedded
+ 2. [x] Checked ordered task
+ 3. [~] Inapplicable ordered task
+ 4. [~] Inapplicable ordered task with ~~del~~ and <s>strike</s> embedded
GFM
<<~GFM
* [ ] Unchecked loose list task
@@ -496,15 +496,14 @@ RSpec.describe 'Copy as GFM', :js, feature_category: :team_planning do
<sub>sub</sub>
<dl>
- <dt>dt</dt>
- <dt>dt</dt>
- <dd>dd</dd>
- <dd>dd</dd>
-
- <dt>dt</dt>
- <dt>dt</dt>
- <dd>dd</dd>
- <dd>dd</dd>
+ <dt>dt</dt>
+ <dt>dt</dt>
+ <dd>dd</dd>
+ <dd>dd</dd>
+ <dt>dt</dt>
+ <dt>dt</dt>
+ <dd>dd</dd>
+ <dd>dd</dd>
</dl>
<kbd>kbd</kbd>
@@ -518,9 +517,8 @@ RSpec.describe 'Copy as GFM', :js, feature_category: :team_planning do
<abbr title="HyperText &quot;Markup&quot; Language">HTML</abbr>
<details>
- <summary>summary></summary>
-
- details
+ <summary>summary</summary>
+ details
</details>
GFM
)
@@ -542,17 +540,20 @@ RSpec.describe 'Copy as GFM', :js, feature_category: :team_planning do
<<~GFM
Foo
- ```js
- Code goes here
- ```
+ ````
+ ```js
+ Code goes here
+ ```
+ ````
GFM
)
verify(
'MarkdownFilter',
- "Line with two spaces at the end \nto insert a linebreak",
+ "Line with backslash at the end\\\nto insert a linebreak",
'`code`',
- '`` code with ` ticks ``',
+ '``code with ` ticks``',
+ '`` `nested` code ``',
'> Quote',
# multiline quote
<<~GFM,
@@ -589,7 +590,7 @@ RSpec.describe 'Copy as GFM', :js, feature_category: :team_planning do
GFM
<<~GFM,
1. Ordered list item
- 1. Ordered list item 2
+ 2. Ordered list item 2
GFM
# multiline ordered list item
@@ -618,7 +619,7 @@ RSpec.describe 'Copy as GFM', :js, feature_category: :team_planning do
'##### Heading',
'###### Heading',
'**Bold**',
- '*Italics*',
+ '_Italics_',
'~~Strikethrough (del)~~',
'<s>Strikethrough</s>',
'---',
diff --git a/spec/frontend/behaviors/copy_as_gfm_spec.js b/spec/frontend/behaviors/copy_as_gfm_spec.js
index 2032faa1c33..a35cf2f0b13 100644
--- a/spec/frontend/behaviors/copy_as_gfm_spec.js
+++ b/spec/frontend/behaviors/copy_as_gfm_spec.js
@@ -1,6 +1,8 @@
import waitForPromises from 'helpers/wait_for_promises';
import initCopyAsGFM, { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm';
+jest.mock('~/emoji');
+
describe('CopyAsGFM', () => {
describe('CopyAsGFM.pasteGFM', () => {
let target;
@@ -116,7 +118,7 @@ describe('CopyAsGFM', () => {
window.getSelection = jest.fn(() => selection);
await simulateCopy();
- const expectedGFM = '1. List Item1\n1. List Item2';
+ const expectedGFM = '1. List Item1\n2. List Item2';
expect(clipboardData.setData).toHaveBeenCalledWith('text/x-gfm', expectedGFM);
});
diff --git a/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js b/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js
index 6db99e796d6..3951714c64e 100644
--- a/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js
+++ b/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js
@@ -11,6 +11,8 @@ jest.mock('~/lib/utils/common_utils', () => ({
getSelectedFragment: jest.fn().mockName('getSelectedFragment'),
}));
+jest.mock('~/emoji');
+
describe('ShortcutsIssuable', () => {
beforeAll(() => {
initCopyAsGFM();
@@ -215,7 +217,7 @@ describe('ShortcutsIssuable', () => {
ShortcutsIssuable.replyWithSelectedText(true);
await waitForPromises();
- expect($(FORM_SELECTOR).val()).toBe('> *Selected text.*\n\n');
+ expect($(FORM_SELECTOR).val()).toBe('> _Selected text._\n\n');
});
it('triggers `focus`', async () => {
diff --git a/spec/frontend/content_editor/extensions/code_suggestion_spec.js b/spec/frontend/content_editor/extensions/code_suggestion_spec.js
index 86656fb96c3..b513735af22 100644
--- a/spec/frontend/content_editor/extensions/code_suggestion_spec.js
+++ b/spec/frontend/content_editor/extensions/code_suggestion_spec.js
@@ -28,7 +28,7 @@ describe('content_editor/extensions/code_suggestion', () => {
let doc;
let codeSuggestion;
- const codeSuggestionConfig = {
+ const codeSuggestionsConfig = {
canSuggest: true,
line: { new_line: 5 },
lines: [{ new_line: 5 }],
@@ -43,7 +43,9 @@ describe('content_editor/extensions/code_suggestion', () => {
tiptapEditor = createTestEditor({
extensions: [
CodeBlockHighlight,
- CodeSuggestion.configure({ config: { ...codeSuggestionConfig, ...config } }),
+ CodeSuggestion.configure({
+ codeSuggestionsConfig: { ...codeSuggestionsConfig, ...config },
+ }),
],
});
diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js
index 4ae39f7a5a7..aeaa63a332f 100644
--- a/spec/frontend/content_editor/services/markdown_serializer_spec.js
+++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js
@@ -138,10 +138,10 @@ const {
},
});
-const serialize = (...content) =>
- new MarkdownSerializer().serialize({
- doc: doc(...content),
- });
+const serializeWithOptions = (options, ...content) =>
+ new MarkdownSerializer().serialize({ doc: doc(...content) }, options);
+
+const serialize = (...content) => new MarkdownSerializer().serialize({ doc: doc(...content) });
describe('markdownSerializer', () => {
it('correctly serializes bold', () => {
@@ -389,6 +389,16 @@ describe('markdownSerializer', () => {
expect(serialize(paragraph(strike('deleted content')))).toBe('~~deleted content~~');
});
+ it.each`
+ strikeTag
+ ${'s'}
+ ${'strike'}
+ `('correctly serializes strikethrough with "$strikeTag" tag', ({ strikeTag }) => {
+ expect(serialize(paragraph(strike({ htmlTag: strikeTag }, 'deleted content')))).toBe(
+ `<${strikeTag}>deleted content</${strikeTag}>`,
+ );
+ });
+
it('correctly serializes blockquotes with hard breaks', () => {
expect(serialize(blockquote('some text', hardBreak(), hardBreak(), 'new line'))).toBe(
`
@@ -411,6 +421,32 @@ describe('markdownSerializer', () => {
);
});
+ it('correctly serializes a blockquote with a nested blockquote', () => {
+ expect(
+ serialize(
+ blockquote(
+ paragraph('some paragraph'),
+ blockquote(paragraph('nested paragraph'), codeBlock('var x = 10;')),
+ ),
+ ),
+ ).toBe(
+ `
+> some paragraph
+>
+> > nested paragraph
+> >
+> > \`\`\`
+> > var x = 10;
+> > \`\`\`
+ `.trim(),
+ );
+ });
+
+ it('skips serializing an empty blockquote if skipEmptyNodes=true', () => {
+ expect(serializeWithOptions({ skipEmptyNodes: true }, blockquote())).toBe('');
+ expect(serializeWithOptions({ skipEmptyNodes: true }, blockquote(paragraph()))).toBe('');
+ });
+
it('correctly serializes a multiline blockquote', () => {
expect(
serialize(
@@ -451,6 +487,23 @@ this is not really json but just trying out whether this case works or not
);
});
+ it('renders a plaintext code block without a prefix', () => {
+ expect(
+ serialize(
+ codeBlock(
+ { language: 'plaintext', langParams: '' },
+ 'this is not really json but just trying out whether this case works or not',
+ ),
+ ),
+ ).toBe(
+ `
+\`\`\`
+this is not really json but just trying out whether this case works or not
+\`\`\`
+ `.trim(),
+ );
+ });
+
it('correctly serializes a code block with language parameters', () => {
expect(
serialize(
@@ -514,6 +567,10 @@ var a = 0;
);
});
+ it('skips serializing an empty code block if skipEmptyNodes=true', () => {
+ expect(serializeWithOptions({ skipEmptyNodes: true }, codeBlock())).toBe('');
+ });
+
it('correctly serializes emoji', () => {
expect(serialize(paragraph(emoji({ name: 'dog' })))).toBe(':dog:');
});
@@ -545,6 +602,20 @@ var a = 0;
);
});
+ it('skips serializing an empty heading if skipEmptyNodes=true', () => {
+ expect(
+ serializeWithOptions(
+ { skipEmptyNodes: true },
+ heading({ level: 1 }),
+ heading({ level: 2 }),
+ heading({ level: 3 }),
+ heading({ level: 4 }),
+ heading({ level: 5 }),
+ heading({ level: 6 }),
+ ),
+ ).toBe('');
+ });
+
it('correctly serializes horizontal rule', () => {
expect(serialize(horizontalRule(), horizontalRule(), horizontalRule())).toBe(
`
@@ -614,6 +685,22 @@ var a = 0;
).toBe('![this is an image](file.png "foo bar baz")');
});
+ it('does not use the canonicalSrc if options.useCanonicalSrc=false', () => {
+ expect(
+ serializeWithOptions(
+ { useCanonicalSrc: false },
+ paragraph(
+ image({
+ src: '/uploads/abcde/file.png',
+ alt: 'this is an image',
+ canonicalSrc: 'file.png',
+ title: 'foo bar baz',
+ }),
+ ),
+ ),
+ ).toBe('![this is an image](/uploads/abcde/file.png "foo bar baz")');
+ });
+
it('correctly serializes bullet list', () => {
expect(
serialize(
diff --git a/spec/lib/gitlab/current_settings_spec.rb b/spec/lib/gitlab/current_settings_spec.rb
index 7fc50438c95..17b2218e05c 100644
--- a/spec/lib/gitlab/current_settings_spec.rb
+++ b/spec/lib/gitlab/current_settings_spec.rb
@@ -88,6 +88,8 @@ RSpec.describe Gitlab::CurrentSettings, feature_category: :shared do
end
describe '#current_application_settings', :use_clean_rails_memory_store_caching do
+ let_it_be(:organization_settings) { create(:organization_setting, restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL]) }
+
it 'allows keys to be called directly' do
described_class.update!(home_page_url: 'http://mydomain.com', signup_enabled: false)
@@ -97,10 +99,58 @@ RSpec.describe Gitlab::CurrentSettings, feature_category: :shared do
expect(described_class.metrics_sample_interval).to be(15)
end
- it 'retrieves settings using ApplicationSettingFetcher' do
- expect(Gitlab::ApplicationSettingFetcher).to receive(:current_application_settings).and_call_original
+ context 'when key is in ApplicationSettingFetcher' do
+ it 'retrieves settings using ApplicationSettingFetcher' do
+ expect(Gitlab::ApplicationSettingFetcher).to receive(:current_application_settings).and_call_original
+
+ described_class.home_page_url
+ end
+ end
+
+ context 'when key is in OrganizationSetting' do
+ before do
+ allow(Gitlab::ApplicationSettingFetcher).to receive(:current_application_settings).and_return(nil)
+ end
+
+ context 'and the current organization is known' do
+ before do
+ allow(Organizations::OrganizationSetting).to receive(:for_current_organization).and_return(organization_settings)
+ end
+
+ it 'retrieves settings using OrganizationSetting' do
+ expect(described_class.restricted_visibility_levels).to eq(organization_settings.restricted_visibility_levels)
+ end
+ end
+
+ context 'and the current organization is unknown' do
+ before do
+ allow(Organizations::OrganizationSetting).to receive(:for_current_organization).and_return(nil)
+ end
+
+ it 'raises NoMethodError' do
+ expect { described_class.foo }.to raise_error(NoMethodError)
+ end
+ end
+ end
+
+ context 'when key is in both sources' do
+ it 'for test purposes, ensure the values are different' do
+ expect(
+ Gitlab::ApplicationSettingFetcher.current_application_settings.restricted_visibility_levels
+ ).not_to eq(organization_settings.restricted_visibility_levels)
+ end
- described_class.home_page_url
+ it 'prefers ApplicationSettingFetcher' do
+ expect(described_class.restricted_visibility_levels).to eq(
+ Gitlab::ApplicationSettingFetcher.current_application_settings.restricted_visibility_levels
+ )
+ end
+ end
+
+ context 'when key is in neither' do
+ it 'raises NoMethodError' do
+ expect { described_class.foo }.to raise_error(NoMethodError)
+ end
end
end
diff --git a/spec/models/organizations/organization_setting_spec.rb b/spec/models/organizations/organization_setting_spec.rb
index 376d0b7fe77..dd90a4d0f58 100644
--- a/spec/models/organizations/organization_setting_spec.rb
+++ b/spec/models/organizations/organization_setting_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe Organizations::OrganizationSetting, type: :model, feature_category: :cell do
let_it_be(:organization) { create(:organization) }
+ let_it_be(:organization_setting) { create(:organization_setting, organization: organization) }
describe 'associations' do
it { is_expected.to belong_to :organization }
@@ -54,4 +55,41 @@ RSpec.describe Organizations::OrganizationSetting, type: :model, feature_categor
end
end
end
+
+ describe '.for_current_organization' do
+ let(:dummy_class) { Class.new { extend Organization::CurrentOrganization } }
+
+ subject(:settings) { described_class.for_current_organization }
+
+ context 'when there is no current organization' do
+ it { is_expected.to be_nil }
+ end
+
+ context 'when there is a current organization' do
+ before do
+ dummy_class.current_organization = organization
+ end
+
+ after do
+ dummy_class.current_organization = nil
+ end
+
+ it 'returns current organization' do
+ expect(settings).to eq(organization_setting)
+ end
+
+ context 'when current organization does not have settings' do
+ before do
+ allow(organization).to receive(:settings).and_return(nil)
+ end
+
+ it 'returns new settings record' do
+ new_settings = settings
+
+ expect(new_settings.organization).to eq(organization)
+ expect(new_settings.new_record?).to eq(true)
+ end
+ end
+ end
+ end
end
diff --git a/spec/scripts/lib/glfm/update_example_snapshots_spec.rb b/spec/scripts/lib/glfm/update_example_snapshots_spec.rb
index 4897dbea09c..78cdb81f80c 100644
--- a/spec/scripts/lib/glfm/update_example_snapshots_spec.rb
+++ b/spec/scripts/lib/glfm/update_example_snapshots_spec.rb
@@ -746,7 +746,10 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process', feature_category: :team
"type": "text",
"marks": [
{
- "type": "strike"
+ "type": "strike",
+ "attrs": {
+ "htmlTag": null
+ }
}
],
"text": "Hi"