summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-08-19 21:10:32 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-08-19 21:10:32 +0000
commitacb2f0ab9452ced85e818d05b4bc5fcc4091959f (patch)
treee8f414d8f4c3daa415455cb772bdaa76e69f3845
parent21db5294d4ba402f9d44a1f59e8344daef0911a2 (diff)
downloadgitlab-ce-acb2f0ab9452ced85e818d05b4bc5fcc4091959f.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop.yml3
-rw-r--r--app/assets/javascripts/content_editor/extensions/table_cell.js3
-rw-r--r--app/assets/javascripts/content_editor/extensions/table_header.js3
-rw-r--r--app/assets/javascripts/content_editor/services/feature_flags.js3
-rw-r--r--app/assets/javascripts/content_editor/services/markdown_serializer.js69
-rw-r--r--app/assets/javascripts/content_editor/services/serialization_helpers.js250
-rw-r--r--app/assets/javascripts/diffs/components/diff_view.vue5
-rw-r--r--app/assets/stylesheets/framework/diffs.scss2
-rw-r--r--app/controllers/projects/wikis_controller.rb4
-rw-r--r--config/feature_flags/development/content_editor_block_tables.yml8
-rw-r--r--lib/gitlab/search_results.rb2
-rw-r--r--spec/frontend/content_editor/services/markdown_serializer_spec.js480
-rw-r--r--spec/frontend/fixtures/api_markdown.yml12
-rw-r--r--spec/lib/gitlab/search_results_spec.rb4
14 files changed, 776 insertions, 72 deletions
diff --git a/.rubocop.yml b/.rubocop.yml
index 7b2b8ca70f5..a82910d89fb 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -38,7 +38,8 @@ AllCops:
- 'workhorse/**/*'
- 'spec/support/*.git/**/*' # e.g. spec/support/gitlab-git-test.git
- 'db/ci_migrate/*.rb' # since the `db/ci_migrate` is a symlinked to `db/migrate`
- CacheRootDirectory: tmp
+ # Use absolute path to avoid orphan directories with changed workspace root.
+ CacheRootDirectory: <%= Dir.getwd %>/tmp
MaxFilesInCache: 25000
Cop/AvoidKeywordArgumentsInSidekiqWorkers:
diff --git a/app/assets/javascripts/content_editor/extensions/table_cell.js b/app/assets/javascripts/content_editor/extensions/table_cell.js
index 5bdc39231a1..b77e6a1c8bb 100644
--- a/app/assets/javascripts/content_editor/extensions/table_cell.js
+++ b/app/assets/javascripts/content_editor/extensions/table_cell.js
@@ -1,5 +1,6 @@
import { TableCell } from '@tiptap/extension-table-cell';
+import { isBlockTablesFeatureEnabled } from '../services/feature_flags';
export default TableCell.extend({
- content: 'inline*',
+ content: isBlockTablesFeatureEnabled() ? 'block+' : 'inline*',
});
diff --git a/app/assets/javascripts/content_editor/extensions/table_header.js b/app/assets/javascripts/content_editor/extensions/table_header.js
index 23509706e4b..513e3da4706 100644
--- a/app/assets/javascripts/content_editor/extensions/table_header.js
+++ b/app/assets/javascripts/content_editor/extensions/table_header.js
@@ -1,5 +1,6 @@
import { TableHeader } from '@tiptap/extension-table-header';
+import { isBlockTablesFeatureEnabled } from '../services/feature_flags';
export default TableHeader.extend({
- content: 'inline*',
+ content: isBlockTablesFeatureEnabled() ? 'block+' : 'inline*',
});
diff --git a/app/assets/javascripts/content_editor/services/feature_flags.js b/app/assets/javascripts/content_editor/services/feature_flags.js
new file mode 100644
index 00000000000..5f7a4595938
--- /dev/null
+++ b/app/assets/javascripts/content_editor/services/feature_flags.js
@@ -0,0 +1,3 @@
+export function isBlockTablesFeatureEnabled() {
+ return gon.features?.contentEditorBlockTables;
+}
diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js
index df4d31c3d7f..57e8de2914b 100644
--- a/app/assets/javascripts/content_editor/services/markdown_serializer.js
+++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js
@@ -30,6 +30,12 @@ import TableRow from '../extensions/table_row';
import TaskItem from '../extensions/task_item';
import TaskList from '../extensions/task_list';
import Text from '../extensions/text';
+import {
+ renderHardBreak,
+ renderTable,
+ renderTableCell,
+ renderTableRow,
+} from './serialization_helpers';
const defaultSerializerConfig = {
marks: {
@@ -65,6 +71,7 @@ const defaultSerializerConfig = {
expelEnclosingWhitespace: true,
},
},
+
nodes: {
[Blockquote.name]: defaultMarkdownSerializer.nodes.blockquote,
[BulletList.name]: defaultMarkdownSerializer.nodes.bullet_list,
@@ -80,7 +87,7 @@ const defaultSerializerConfig = {
state.write(`:${name}:`);
},
- [HardBreak.name]: defaultMarkdownSerializer.nodes.hard_break,
+ [HardBreak.name]: renderHardBreak,
[Heading.name]: defaultMarkdownSerializer.nodes.heading,
[HorizontalRule.name]: defaultMarkdownSerializer.nodes.horizontal_rule,
[Image.name]: (state, node) => {
@@ -95,60 +102,10 @@ const defaultSerializerConfig = {
[Reference.name]: (state, node) => {
state.write(node.attrs.originalText || node.attrs.text);
},
- [Table.name]: (state, node) => {
- state.renderContent(node);
- },
- [TableCell.name]: (state, node) => {
- state.renderInline(node);
- },
- [TableHeader.name]: (state, node) => {
- state.renderInline(node);
- },
- [TableRow.name]: (state, node) => {
- const isHeaderRow = node.child(0).type.name === 'tableHeader';
-
- const renderRow = () => {
- 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;
- };
-
- const renderHeaderRow = (cellWidths) => {
- state.flushClose(1);
-
- state.write('|');
- node.forEach((cell, _, i) => {
- if (i) state.write('|');
-
- state.write(cell.attrs.align === 'center' ? ':' : '-');
- state.write(state.repeat('-', cellWidths[i]));
- state.write(cell.attrs.align === 'center' || cell.attrs.align === 'right' ? ':' : '-');
- });
- state.write('|');
-
- state.closeBlock(node);
- };
-
- if (isHeaderRow) {
- renderHeaderRow(renderRow());
- } else {
- renderRow();
- }
- },
+ [Table.name]: renderTable,
+ [TableCell.name]: renderTableCell,
+ [TableHeader.name]: renderTableCell,
+ [TableRow.name]: renderTableRow,
[TaskItem.name]: (state, node) => {
state.write(`[${node.attrs.checked ? 'x' : ' '}] `);
state.renderContent(node);
@@ -175,7 +132,7 @@ const wrapHtmlPayload = (payload) => `<div>${payload}</div>`;
* that parses the Markdown and converts it into HTML.
* @returns a markdown serializer
*/
-export default ({ render = () => null, serializerConfig }) => ({
+export default ({ render = () => null, serializerConfig = {} } = {}) => ({
/**
* Converts a Markdown string into a ProseMirror JSONDocument based
* on a ProseMirror schema.
diff --git a/app/assets/javascripts/content_editor/services/serialization_helpers.js b/app/assets/javascripts/content_editor/services/serialization_helpers.js
new file mode 100644
index 00000000000..909ab3dbd68
--- /dev/null
+++ b/app/assets/javascripts/content_editor/services/serialization_helpers.js
@@ -0,0 +1,250 @@
+import { uniq } from 'lodash';
+import { isBlockTablesFeatureEnabled } from './feature_flags';
+
+const defaultAttrs = {
+ td: { colspan: 1, rowspan: 1, colwidth: null },
+ th: { colspan: 1, rowspan: 1, colwidth: null },
+};
+
+const tableMap = new WeakMap();
+
+function shouldRenderCellInline(cell) {
+ if (cell.childCount === 1) {
+ const parent = cell.child(0);
+ if (parent.type.name === 'paragraph' && parent.childCount === 1) {
+ const child = parent.child(0);
+ return child.isText && child.marks.length === 0;
+ }
+ }
+
+ return false;
+}
+
+function getRowsAndCells(table) {
+ const cells = [];
+ const rows = [];
+ table.descendants((n) => {
+ if (n.type.name === 'tableCell' || n.type.name === 'tableHeader') {
+ cells.push(n);
+ return false;
+ }
+
+ if (n.type.name === 'tableRow') {
+ rows.push(n);
+ }
+
+ return true;
+ });
+ return { rows, cells };
+}
+
+function getChildren(node) {
+ const children = [];
+ for (let i = 0; i < node.childCount; i += 1) {
+ children.push(node.child(i));
+ }
+ return children;
+}
+
+function shouldRenderHTMLTable(table) {
+ const { rows, cells } = getRowsAndCells(table);
+
+ const cellChildCount = Math.max(...cells.map((cell) => cell.childCount));
+ const maxColspan = Math.max(...cells.map((cell) => cell.attrs.colspan));
+ const maxRowspan = Math.max(...cells.map((cell) => cell.attrs.rowspan));
+
+ const rowChildren = rows.map((row) => uniq(getChildren(row).map((cell) => cell.type.name)));
+ const cellTypeInFirstRow = rowChildren[0];
+ const cellTypesInOtherRows = uniq(rowChildren.slice(1).map(([type]) => type));
+
+ // if the first row has headers, and there are no headers anywhere else, render markdown table
+ if (
+ !(
+ cellTypeInFirstRow.length === 1 &&
+ cellTypeInFirstRow[0] === 'tableHeader' &&
+ cellTypesInOtherRows.length === 1 &&
+ cellTypesInOtherRows[0] === 'tableCell'
+ )
+ ) {
+ return true;
+ }
+
+ if (cellChildCount === 1 && maxColspan === 1 && maxRowspan === 1) {
+ // if all rows contain only one paragraph each and no rowspan/colspan, render markdown table
+ const children = uniq(cells.map((cell) => cell.child(0).type.name));
+ if (children.length === 1 && children[0] === 'paragraph') {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+function openTag(state, tagName, attrs) {
+ let str = `<${tagName}`;
+
+ str += Object.entries(attrs || {})
+ .map(([key, value]) => {
+ if (defaultAttrs[tagName]?.[key] === value) return '';
+
+ return ` ${key}=${state.quote(value?.toString() || '')}`;
+ })
+ .join('');
+
+ return `${str}>`;
+}
+
+function closeTag(state, tagName) {
+ return `</${tagName}>`;
+}
+
+function isInBlockTable(node) {
+ return tableMap.get(node);
+}
+
+function isInTable(node) {
+ return tableMap.has(node);
+}
+
+function setIsInBlockTable(table, value) {
+ tableMap.set(table, value);
+
+ const { rows, cells } = getRowsAndCells(table);
+ rows.forEach((row) => tableMap.set(row, value));
+ cells.forEach((cell) => {
+ tableMap.set(cell, value);
+ if (cell.childCount && cell.child(0).type.name === 'paragraph')
+ tableMap.set(cell.child(0), value);
+ });
+}
+
+function unsetIsInBlockTable(table) {
+ tableMap.delete(table);
+
+ const { rows, cells } = getRowsAndCells(table);
+ rows.forEach((row) => tableMap.delete(row));
+ cells.forEach((cell) => {
+ tableMap.delete(cell);
+ if (cell.childCount) tableMap.delete(cell.child(0));
+ });
+}
+
+function renderTagOpen(state, tagName, attrs) {
+ state.ensureNewLine();
+ state.write(openTag(state, tagName, attrs));
+}
+
+function renderTagClose(state, tagName, insertNewline = true) {
+ state.write(closeTag(state, tagName));
+ if (insertNewline) state.ensureNewLine();
+}
+
+function renderTableHeaderRowAsMarkdown(state, node, cellWidths) {
+ state.flushClose(1);
+
+ state.write('|');
+ node.forEach((cell, _, i) => {
+ if (i) state.write('|');
+
+ state.write(cell.attrs.align === 'center' ? ':' : '-');
+ state.write(state.repeat('-', cellWidths[i]));
+ state.write(cell.attrs.align === 'center' || cell.attrs.align === 'right' ? ':' : '-');
+ });
+ state.write('|');
+
+ state.closeBlock(node);
+}
+
+function renderTableRowAsMarkdown(state, node, isHeaderRow = false) {
+ 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);
+
+ if (isHeaderRow) renderTableHeaderRowAsMarkdown(state, node, cellWidths);
+}
+
+function renderTableRowAsHTML(state, node) {
+ renderTagOpen(state, 'tr');
+
+ node.forEach((cell, _, i) => {
+ const tag = cell.type.name === 'tableHeader' ? 'th' : 'td';
+
+ renderTagOpen(state, tag, cell.attrs);
+
+ if (!shouldRenderCellInline(cell)) {
+ state.closeBlock(node);
+ state.flushClose();
+ }
+
+ state.render(cell, node, i);
+ state.flushClose(1);
+
+ renderTagClose(state, tag);
+ });
+
+ renderTagClose(state, 'tr');
+}
+
+export function renderTableCell(state, node) {
+ if (!isBlockTablesFeatureEnabled()) {
+ state.renderInline(node);
+ return;
+ }
+
+ if (!isInBlockTable(node) || shouldRenderCellInline(node)) {
+ state.renderInline(node.child(0));
+ } else {
+ state.renderContent(node);
+ }
+}
+
+export function renderTableRow(state, node) {
+ if (isInBlockTable(node)) {
+ renderTableRowAsHTML(state, node);
+ } else {
+ renderTableRowAsMarkdown(state, node, node.child(0).type.name === 'tableHeader');
+ }
+}
+
+export function renderTable(state, node) {
+ if (isBlockTablesFeatureEnabled()) {
+ setIsInBlockTable(node, shouldRenderHTMLTable(node));
+ }
+
+ if (isInBlockTable(node)) renderTagOpen(state, 'table');
+
+ state.renderContent(node);
+
+ if (isInBlockTable(node)) renderTagClose(state, 'table');
+
+ // ensure at least one blank line after any table
+ state.closeBlock(node);
+ state.flushClose();
+
+ if (isBlockTablesFeatureEnabled()) {
+ unsetIsInBlockTable(node);
+ }
+}
+
+export function renderHardBreak(state, node, parent, index) {
+ const br = isInTable(parent) ? '<br>' : '\\\n';
+
+ for (let i = index + 1; i < parent.childCount; i += 1) {
+ if (parent.child(i).type !== node.type) {
+ state.write(br);
+ return;
+ }
+ }
+}
diff --git a/app/assets/javascripts/diffs/components/diff_view.vue b/app/assets/javascripts/diffs/components/diff_view.vue
index 5cf242b4ddd..64ded1ca8ca 100644
--- a/app/assets/javascripts/diffs/components/diff_view.vue
+++ b/app/assets/javascripts/diffs/components/diff_view.vue
@@ -133,7 +133,10 @@ export default {
<template>
<div
- :class="[$options.userColorScheme, { inline, 'with-codequality': hasCodequalityChanges }]"
+ :class="[
+ $options.userColorScheme,
+ { 'inline-diff-view': inline, 'with-codequality': hasCodequalityChanges },
+ ]"
:data-commit-id="commitId"
class="diff-grid diff-table code diff-wrap-lines js-syntax-highlight text-file"
@mousedown="handleParallelLineMouseDown"
diff --git a/app/assets/stylesheets/framework/diffs.scss b/app/assets/stylesheets/framework/diffs.scss
index 61a20c7a8fd..9adb8532a93 100644
--- a/app/assets/stylesheets/framework/diffs.scss
+++ b/app/assets/stylesheets/framework/diffs.scss
@@ -613,7 +613,7 @@ table.code {
grid-template-columns: 1fr 1fr;
}
- &.inline {
+ &.inline-diff-view {
.diff-grid-comments {
display: grid;
grid-template-columns: 1fr;
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index d1486f765e4..9ee8847004e 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -5,5 +5,9 @@ class Projects::WikisController < Projects::ApplicationController
alias_method :container, :project
+ before_action do
+ push_frontend_feature_flag(:content_editor_block_tables, @project, default_enabled: :yaml)
+ end
+
feature_category :wiki
end
diff --git a/config/feature_flags/development/content_editor_block_tables.yml b/config/feature_flags/development/content_editor_block_tables.yml
new file mode 100644
index 00000000000..176422bbc92
--- /dev/null
+++ b/config/feature_flags/development/content_editor_block_tables.yml
@@ -0,0 +1,8 @@
+---
+name: content_editor_block_tables
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66187
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338937
+milestone: '14.3'
+type: development
+group: group::editor
+default_enabled: false
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index 90513e346f2..217a48e740d 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -188,7 +188,7 @@ module Gitlab
merge_requests = MergeRequestsFinder.new(current_user, issuable_params).execute
unless default_project_filter
- merge_requests = merge_requests.in_projects(project_ids_relation)
+ merge_requests = merge_requests.of_projects(project_ids_relation)
end
apply_sort(merge_requests, scope: 'merge_requests')
diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js
new file mode 100644
index 00000000000..24b3779057c
--- /dev/null
+++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js
@@ -0,0 +1,480 @@
+import Blockquote from '~/content_editor/extensions/blockquote';
+import Bold from '~/content_editor/extensions/bold';
+import BulletList from '~/content_editor/extensions/bullet_list';
+import Code from '~/content_editor/extensions/code';
+import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
+import Emoji from '~/content_editor/extensions/emoji';
+import HardBreak from '~/content_editor/extensions/hard_break';
+import Heading from '~/content_editor/extensions/heading';
+import HorizontalRule from '~/content_editor/extensions/horizontal_rule';
+import Image from '~/content_editor/extensions/image';
+import Italic from '~/content_editor/extensions/italic';
+import Link from '~/content_editor/extensions/link';
+import ListItem from '~/content_editor/extensions/list_item';
+import OrderedList from '~/content_editor/extensions/ordered_list';
+import Paragraph from '~/content_editor/extensions/paragraph';
+import Strike from '~/content_editor/extensions/strike';
+import Table from '~/content_editor/extensions/table';
+import TableCell from '~/content_editor/extensions/table_cell';
+import TableHeader from '~/content_editor/extensions/table_header';
+import TableRow from '~/content_editor/extensions/table_row';
+import Text from '~/content_editor/extensions/text';
+import markdownSerializer from '~/content_editor/services/markdown_serializer';
+import { createTestEditor, createDocBuilder } from '../test_utils';
+
+jest.mock('~/emoji');
+
+jest.mock('~/content_editor/services/feature_flags', () => ({
+ isBlockTablesFeatureEnabled: jest.fn().mockReturnValue(true),
+}));
+
+const tiptapEditor = createTestEditor({
+ extensions: [
+ Blockquote,
+ Bold,
+ BulletList,
+ Code,
+ CodeBlockHighlight,
+ Emoji,
+ HardBreak,
+ Heading,
+ HorizontalRule,
+ Image,
+ Italic,
+ Link,
+ ListItem,
+ OrderedList,
+ Paragraph,
+ Strike,
+ Table,
+ TableCell,
+ TableHeader,
+ TableRow,
+ Text,
+ ],
+});
+
+const {
+ builders: {
+ doc,
+ blockquote,
+ bold,
+ bulletList,
+ code,
+ codeBlock,
+ emoji,
+ heading,
+ hardBreak,
+ horizontalRule,
+ image,
+ italic,
+ link,
+ listItem,
+ orderedList,
+ paragraph,
+ strike,
+ table,
+ tableCell,
+ tableHeader,
+ tableRow,
+ },
+} = createDocBuilder({
+ tiptapEditor,
+ names: {
+ blockquote: { nodeType: Blockquote.name },
+ bold: { markType: Bold.name },
+ bulletList: { nodeType: BulletList.name },
+ code: { markType: Code.name },
+ codeBlock: { nodeType: CodeBlockHighlight.name },
+ emoji: { markType: Emoji.name },
+ hardBreak: { nodeType: HardBreak.name },
+ heading: { nodeType: Heading.name },
+ horizontalRule: { nodeType: HorizontalRule.name },
+ image: { nodeType: Image.name },
+ italic: { nodeType: Italic.name },
+ link: { markType: Link.name },
+ listItem: { nodeType: ListItem.name },
+ orderedList: { nodeType: OrderedList.name },
+ paragraph: { nodeType: Paragraph.name },
+ strike: { markType: Strike.name },
+ table: { nodeType: Table.name },
+ tableCell: { nodeType: TableCell.name },
+ tableHeader: { nodeType: TableHeader.name },
+ tableRow: { nodeType: TableRow.name },
+ },
+});
+
+const serialize = (...content) =>
+ markdownSerializer({}).serialize({
+ schema: tiptapEditor.schema,
+ content: doc(...content).toJSON(),
+ });
+
+describe('markdownSerializer', () => {
+ it('correctly serializes a line break', () => {
+ expect(serialize(paragraph('hello', hardBreak(), 'world'))).toBe('hello\\\nworld');
+ });
+
+ it('correctly serializes a table with inline content', () => {
+ expect(
+ serialize(
+ table(
+ // each table cell must contain at least one paragraph
+ tableRow(
+ tableHeader(paragraph('header')),
+ tableHeader(paragraph('header')),
+ tableHeader(paragraph('header')),
+ ),
+ tableRow(
+ tableCell(paragraph('cell')),
+ tableCell(paragraph('cell')),
+ tableCell(paragraph('cell')),
+ ),
+ tableRow(
+ tableCell(paragraph('cell')),
+ tableCell(paragraph('cell')),
+ tableCell(paragraph('cell')),
+ ),
+ ),
+ ).trim(),
+ ).toBe(
+ `
+| header | header | header |
+|--------|--------|--------|
+| cell | cell | cell |
+| cell | cell | cell |
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes a table with line breaks', () => {
+ expect(
+ serialize(
+ table(
+ tableRow(tableHeader(paragraph('header')), tableHeader(paragraph('header'))),
+ tableRow(
+ tableCell(paragraph('cell with', hardBreak(), 'line', hardBreak(), 'breaks')),
+ tableCell(paragraph('cell')),
+ ),
+ tableRow(tableCell(paragraph('cell')), tableCell(paragraph('cell'))),
+ ),
+ ).trim(),
+ ).toBe(
+ `
+| header | header |
+|--------|--------|
+| cell with<br>line<br>breaks | cell |
+| cell | cell |
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes two consecutive tables', () => {
+ expect(
+ serialize(
+ table(
+ tableRow(tableHeader(paragraph('header')), tableHeader(paragraph('header'))),
+ tableRow(tableCell(paragraph('cell')), tableCell(paragraph('cell'))),
+ tableRow(tableCell(paragraph('cell')), tableCell(paragraph('cell'))),
+ ),
+ table(
+ tableRow(tableHeader(paragraph('header')), tableHeader(paragraph('header'))),
+ tableRow(tableCell(paragraph('cell')), tableCell(paragraph('cell'))),
+ tableRow(tableCell(paragraph('cell')), tableCell(paragraph('cell'))),
+ ),
+ ).trim(),
+ ).toBe(
+ `
+| header | header |
+|--------|--------|
+| cell | cell |
+| cell | cell |
+
+| header | header |
+|--------|--------|
+| cell | cell |
+| cell | cell |
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes a table with block content', () => {
+ expect(
+ serialize(
+ table(
+ tableRow(
+ tableHeader(paragraph('examples of')),
+ tableHeader(paragraph('block content')),
+ tableHeader(paragraph('in tables')),
+ tableHeader(paragraph('in content editor')),
+ ),
+ tableRow(
+ tableCell(heading({ level: 1 }, 'heading 1')),
+ tableCell(heading({ level: 2 }, 'heading 2')),
+ tableCell(paragraph(bold('just bold'))),
+ tableCell(paragraph(bold('bold'), ' ', italic('italic'), ' ', code('code'))),
+ ),
+ tableRow(
+ tableCell(
+ paragraph('all marks in three paragraphs:'),
+ paragraph('the ', bold('quick'), ' ', italic('brown'), ' ', code('fox')),
+ paragraph(
+ link({ href: '/home' }, 'jumps'),
+ ' over the ',
+ strike('lazy'),
+ ' ',
+ emoji({ name: 'dog' }),
+ ),
+ ),
+ tableCell(
+ paragraph(image({ src: 'img.jpg', alt: 'some image' }), hardBreak(), 'image content'),
+ ),
+ tableCell(
+ blockquote('some text', hardBreak(), hardBreak(), 'in a multiline blockquote'),
+ ),
+ tableCell(
+ codeBlock(
+ { language: 'javascript' },
+ 'var a = 2;\nvar b = 3;\nvar c = a + d;\n\nconsole.log(c);',
+ ),
+ ),
+ ),
+ tableRow(
+ tableCell(bulletList(listItem('item 1'), listItem('item 2'), listItem('item 2'))),
+ tableCell(orderedList(listItem('item 1'), listItem('item 2'), listItem('item 2'))),
+ tableCell(
+ paragraph('paragraphs separated by'),
+ horizontalRule(),
+ paragraph('a horizontal rule'),
+ ),
+ tableCell(
+ table(
+ tableRow(tableHeader(paragraph('table')), tableHeader(paragraph('inside'))),
+ tableRow(tableCell(paragraph('another')), tableCell(paragraph('table'))),
+ ),
+ ),
+ ),
+ ),
+ ).trim(),
+ ).toBe(
+ `
+<table>
+<tr>
+<th>examples of</th>
+<th>block content</th>
+<th>in tables</th>
+<th>in content editor</th>
+</tr>
+<tr>
+<td>
+
+# heading 1
+</td>
+<td>
+
+## heading 2
+</td>
+<td>
+
+**just bold**
+</td>
+<td>
+
+**bold** _italic_ \`code\`
+</td>
+</tr>
+<tr>
+<td>
+
+all marks in three paragraphs:
+
+the **quick** _brown_ \`fox\`
+
+[jumps](/home) over the ~~lazy~~ :dog:
+</td>
+<td>
+
+![some image](img.jpg)<br>image content
+</td>
+<td>
+
+> some text\\
+> \\
+> in a multiline blockquote
+</td>
+<td>
+
+\`\`\`javascript
+var a = 2;
+var b = 3;
+var c = a + d;
+
+console.log(c);
+\`\`\`
+</td>
+</tr>
+<tr>
+<td>
+
+* item 1
+* item 2
+* item 2
+</td>
+<td>
+
+1. item 1
+2. item 2
+3. item 2
+</td>
+<td>
+
+paragraphs separated by
+
+---
+
+a horizontal rule
+</td>
+<td>
+
+| table | inside |
+|-------|--------|
+| another | table |
+
+</td>
+</tr>
+</table>
+ `.trim(),
+ );
+ });
+
+ it('correctly renders content after a markdown table', () => {
+ expect(
+ serialize(
+ table(tableRow(tableHeader(paragraph('header'))), tableRow(tableCell(paragraph('cell')))),
+ heading({ level: 1 }, 'this is a heading'),
+ ).trim(),
+ ).toBe(
+ `
+| header |
+|--------|
+| cell |
+
+# this is a heading
+ `.trim(),
+ );
+ });
+
+ it('correctly renders content after an html table', () => {
+ expect(
+ serialize(
+ table(
+ tableRow(tableHeader(paragraph('header'))),
+ tableRow(tableCell(blockquote('hi'), paragraph('there'))),
+ ),
+ heading({ level: 1 }, 'this is a heading'),
+ ).trim(),
+ ).toBe(
+ `
+<table>
+<tr>
+<th>header</th>
+</tr>
+<tr>
+<td>
+
+> hi
+
+there
+</td>
+</tr>
+</table>
+
+# this is a heading
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes tables with misplaced header cells', () => {
+ expect(
+ serialize(
+ table(
+ tableRow(tableHeader(paragraph('cell')), tableCell(paragraph('cell'))),
+ tableRow(tableCell(paragraph('cell')), tableHeader(paragraph('cell'))),
+ ),
+ ).trim(),
+ ).toBe(
+ `
+<table>
+<tr>
+<th>cell</th>
+<td>cell</td>
+</tr>
+<tr>
+<td>cell</td>
+<th>cell</th>
+</tr>
+</table>
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes table without any headers', () => {
+ expect(
+ serialize(
+ table(
+ tableRow(tableCell(paragraph('cell')), tableCell(paragraph('cell'))),
+ tableRow(tableCell(paragraph('cell')), tableCell(paragraph('cell'))),
+ ),
+ ).trim(),
+ ).toBe(
+ `
+<table>
+<tr>
+<td>cell</td>
+<td>cell</td>
+</tr>
+<tr>
+<td>cell</td>
+<td>cell</td>
+</tr>
+</table>
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes table with rowspan and colspan', () => {
+ expect(
+ serialize(
+ table(
+ tableRow(
+ tableHeader(paragraph('header')),
+ tableHeader(paragraph('header')),
+ tableHeader(paragraph('header')),
+ ),
+ tableRow(
+ tableCell({ colspan: 2 }, paragraph('cell with rowspan: 2')),
+ tableCell({ rowspan: 2 }, paragraph('cell')),
+ ),
+ tableRow(tableCell({ colspan: 2 }, paragraph('cell with rowspan: 2'))),
+ ),
+ ).trim(),
+ ).toBe(
+ `
+<table>
+<tr>
+<th>header</th>
+<th>header</th>
+<th>header</th>
+</tr>
+<tr>
+<td colspan="2">cell with rowspan: 2</td>
+<td rowspan="2">cell</td>
+</tr>
+<tr>
+<td colspan="2">cell with rowspan: 2</td>
+</tr>
+</table>
+ `.trim(),
+ );
+ });
+});
diff --git a/spec/frontend/fixtures/api_markdown.yml b/spec/frontend/fixtures/api_markdown.yml
index b581aac6aee..09a57e04631 100644
--- a/spec/frontend/fixtures/api_markdown.yml
+++ b/spec/frontend/fixtures/api_markdown.yml
@@ -102,14 +102,10 @@
markdown: |-
| header | header |
|--------|--------|
- | cell | cell |
- | cell | cell |
-- name: table_with_alignment
- markdown: |-
- | header | : header : | header : |
- |--------|------------|----------|
- | cell | cell | cell |
- | cell | cell | cell |
+ | `code` | cell with **bold** |
+ | ~~strike~~ | cell with _italic_ |
+
+ # content after table
- name: emoji
markdown: ':sparkles: :heart: :100:'
- name: reference
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index b8972f28889..27d65e14347 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -148,13 +148,13 @@ RSpec.describe Gitlab::SearchResults do
end
end
- it 'includes merge requests from source and target projects' do
+ it 'does not include merge requests from source projects' do
forked_project = fork_project(project, user)
merge_request_2 = create(:merge_request, target_project: project, source_project: forked_project, title: 'foo')
results = described_class.new(user, 'foo', Project.where(id: forked_project.id))
- expect(results.objects('merge_requests')).to include merge_request_2
+ expect(results.objects('merge_requests')).not_to include merge_request_2
end
describe '#merge_requests' do