diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-08-19 21:10:32 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-08-19 21:10:32 +0000 |
commit | acb2f0ab9452ced85e818d05b4bc5fcc4091959f (patch) | |
tree | e8f414d8f4c3daa415455cb772bdaa76e69f3845 | |
parent | 21db5294d4ba402f9d44a1f59e8344daef0911a2 (diff) | |
download | gitlab-ce-acb2f0ab9452ced85e818d05b4bc5fcc4091959f.tar.gz |
Add latest changes from gitlab-org/gitlab@master
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 |