8
0
Просмотр исходного кода

Changed: Extract downcastTable() and upcastTable() converters to separate files.

Maciej Gołaszewski 7 лет назад
Родитель
Сommit
87b81f5455

+ 2 - 166
packages/ckeditor5-table/src/converters.js

@@ -4,68 +4,12 @@
  */
 
 /**
- * @module table/converters
+ * @module table/converters/downcasttable
  */
 
 import Position from '@ckeditor/ckeditor5-engine/src/view/position';
-import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range';
-import ModelPosition from '@ckeditor/ckeditor5-engine/src/model/position';
 
-export function upcastTable() {
-	const converter = ( evt, data, conversionApi ) => {
-		const viewTable = data.viewItem;
-
-		// When element was already consumed then skip it.
-		const test = conversionApi.consumable.test( viewTable, { name: true } );
-
-		if ( !test ) {
-			return;
-		}
-
-		const modelTable = conversionApi.writer.createElement( 'table' );
-
-		const splitResult = conversionApi.splitToAllowedParent( modelTable, data.modelCursor );
-
-		// Insert element on allowed position.
-		conversionApi.writer.insert( modelTable, splitResult.position );
-
-		// Convert children and insert to element.
-		_upcastTableRows( viewTable, modelTable, conversionApi );
-
-		// Consume appropriate value from consumable values list.
-		conversionApi.consumable.consume( viewTable, { name: true } );
-
-		// Set conversion result range.
-		data.modelRange = new ModelRange(
-			// Range should start before inserted element
-			ModelPosition.createBefore( modelTable ),
-			// Should end after but we need to take into consideration that children could split our
-			// element, so we need to move range after parent of the last converted child.
-			// before: <allowed>[]</allowed>
-			// after: <allowed>[<converted><child></child></converted><child></child><converted>]</converted></allowed>
-			ModelPosition.createAfter( modelTable )
-		);
-
-		// Now we need to check where the modelCursor should be.
-		// If we had to split parent to insert our element then we want to continue conversion inside split parent.
-		//
-		// before: <allowed><notAllowed>[]</notAllowed></allowed>
-		// after:  <allowed><notAllowed></notAllowed><converted></converted><notAllowed>[]</notAllowed></allowed>
-		if ( splitResult.cursorParent ) {
-			data.modelCursor = ModelPosition.createAt( splitResult.cursorParent );
-
-			// Otherwise just continue after inserted element.
-		} else {
-			data.modelCursor = data.modelRange.end;
-		}
-	};
-
-	return dispatcher => {
-		dispatcher.on( 'element:table', converter, { priority: 'normal' } );
-	};
-}
-
-export function downcastTable() {
+export default function downcastTable() {
 	return dispatcher => dispatcher.on( 'insert:table', ( evt, data, conversionApi ) => {
 		const table = data.item;
 
@@ -163,114 +107,6 @@ function _isHead( tableCell, rowIndex, cellIndex, columnHeadings ) {
 	return ( !!headingRows && headingRows > rowIndex ) || ( !!columnHeadings && columnHeadings > cellIndex );
 }
 
-function _upcastTableRows( viewTable, modelTable, conversionApi ) {
-	const { rows, headingRows, headingColumns } = _scanTable( viewTable );
-
-	for ( const viewRow of rows ) {
-		const modelRow = conversionApi.writer.createElement( 'tableRow' );
-		conversionApi.writer.insert( modelRow, ModelPosition.createAt( modelTable, 'end' ) );
-		conversionApi.consumable.consume( viewRow, { name: true } );
-
-		const childrenCursor = ModelPosition.createAt( modelRow );
-		conversionApi.convertChildren( viewRow, childrenCursor );
-	}
-
-	if ( headingRows ) {
-		conversionApi.writer.setAttribute( 'headingRows', headingRows, modelTable );
-	}
-
-	if ( headingColumns ) {
-		conversionApi.writer.setAttribute( 'headingColumns', headingColumns, modelTable );
-	}
-
-	if ( !rows.length ) {
-		// Create empty table with one row and one table cell.
-		const row = conversionApi.writer.createElement( 'tableRow' );
-		conversionApi.writer.insert( row, ModelPosition.createAt( modelTable, 'end' ) );
-		conversionApi.writer.insertElement( 'tableCell', ModelPosition.createAt( row, 'end' ) );
-	}
-}
-
-// This one scans table rows & extracts required metadata from table:
-//
-// headingRows    - number of rows that goes as table header.
-// headingColumns - max number of row headings.
-// rows           - sorted trs as they should go into the model - ie if <thead> is inserted after <tbody> in the view.
-function _scanTable( viewTable ) {
-	const tableMeta = {
-		headingRows: 0,
-		headingColumns: 0,
-		rows: {
-			head: [],
-			body: []
-		}
-	};
-
-	let firstTheadElement;
-
-	for ( const tableChild of Array.from( viewTable.getChildren() ) ) {
-		// Only <thead>, <tbody> & <tfoot> from allowed table children can have <tr>s.
-		// The else is for future purposes (mainly <caption>).
-		if ( tableChild.name === 'tbody' || tableChild.name === 'thead' || tableChild.name === 'tfoot' ) {
-			// Parse only the first <thead> in the table as table header - all other ones will be converted to table body rows.
-			if ( tableChild.name === 'thead' && !firstTheadElement ) {
-				firstTheadElement = tableChild;
-			}
-
-			for ( const childRow of Array.from( tableChild.getChildren() ) ) {
-				_scanRow( childRow, tableMeta, firstTheadElement );
-			}
-		}
-	}
-
-	// Unify returned table meta.
-	tableMeta.rows = [ ...tableMeta.rows.head, ...tableMeta.rows.body ];
-
-	return tableMeta;
-}
-
-// Scans <tr> and it's children for metadata:
-// - For heading row:
-//     - either add this row to heading or body rows.
-//     - updates number of heading rows.
-// - For body rows:
-//     - calculates number of column headings.
-function _scanRow( tr, tableMeta, firstThead ) {
-	if ( tr.parent.name === 'thead' && tr.parent === firstThead ) {
-		// It's a table header so only update it's meta.
-		tableMeta.headingRows++;
-		tableMeta.rows.head.push( tr );
-
-		return;
-	}
-
-	// For normal row check how many column headings this row has.
-	tableMeta.rows.body.push( tr );
-
-	let headingCols = 0;
-	let index = 0;
-
-	// Filter out empty text nodes from tr children.
-	const children = Array.from( tr.getChildren() )
-		.filter( child => child.name === 'th' || child.name === 'td' );
-
-	// Count starting adjacent <th> elements of a <tr>.
-	while ( index < children.length && children[ index ].name === 'th' ) {
-		const td = children[ index ];
-
-		// Adjust columns calculation by the number of extended columns.
-		const hasAttribute = td.hasAttribute( 'colspan' );
-		const tdSize = hasAttribute ? parseInt( td.getAttribute( 'colspan' ) ) : 1;
-
-		headingCols = headingCols + tdSize;
-		index++;
-	}
-
-	if ( headingCols > tableMeta.headingColumns ) {
-		tableMeta.headingColumns = headingCols;
-	}
-}
-
 /**
  * Holds information about spanned table cells.
  *

+ 173 - 0
packages/ckeditor5-table/src/converters/upcasttable.js

@@ -0,0 +1,173 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module table/converters/upcasttable
+ */
+
+import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range';
+import ModelPosition from '@ckeditor/ckeditor5-engine/src/model/position';
+
+export default function upcastTable() {
+	const converter = ( evt, data, conversionApi ) => {
+		const viewTable = data.viewItem;
+
+		// When element was already consumed then skip it.
+		const test = conversionApi.consumable.test( viewTable, { name: true } );
+
+		if ( !test ) {
+			return;
+		}
+
+		const modelTable = conversionApi.writer.createElement( 'table' );
+
+		const splitResult = conversionApi.splitToAllowedParent( modelTable, data.modelCursor );
+
+		// Insert element on allowed position.
+		conversionApi.writer.insert( modelTable, splitResult.position );
+
+		// Convert children and insert to element.
+		_upcastTableRows( viewTable, modelTable, conversionApi );
+
+		// Consume appropriate value from consumable values list.
+		conversionApi.consumable.consume( viewTable, { name: true } );
+
+		// Set conversion result range.
+		data.modelRange = new ModelRange(
+			// Range should start before inserted element
+			ModelPosition.createBefore( modelTable ),
+			// Should end after but we need to take into consideration that children could split our
+			// element, so we need to move range after parent of the last converted child.
+			// before: <allowed>[]</allowed>
+			// after: <allowed>[<converted><child></child></converted><child></child><converted>]</converted></allowed>
+			ModelPosition.createAfter( modelTable )
+		);
+
+		// Now we need to check where the modelCursor should be.
+		// If we had to split parent to insert our element then we want to continue conversion inside split parent.
+		//
+		// before: <allowed><notAllowed>[]</notAllowed></allowed>
+		// after:  <allowed><notAllowed></notAllowed><converted></converted><notAllowed>[]</notAllowed></allowed>
+		if ( splitResult.cursorParent ) {
+			data.modelCursor = ModelPosition.createAt( splitResult.cursorParent );
+
+			// Otherwise just continue after inserted element.
+		} else {
+			data.modelCursor = data.modelRange.end;
+		}
+	};
+
+	return dispatcher => {
+		dispatcher.on( 'element:table', converter, { priority: 'normal' } );
+	};
+}
+
+function _upcastTableRows( viewTable, modelTable, conversionApi ) {
+	const { rows, headingRows, headingColumns } = _scanTable( viewTable );
+
+	for ( const viewRow of rows ) {
+		const modelRow = conversionApi.writer.createElement( 'tableRow' );
+		conversionApi.writer.insert( modelRow, ModelPosition.createAt( modelTable, 'end' ) );
+		conversionApi.consumable.consume( viewRow, { name: true } );
+
+		const childrenCursor = ModelPosition.createAt( modelRow );
+		conversionApi.convertChildren( viewRow, childrenCursor );
+	}
+
+	if ( headingRows ) {
+		conversionApi.writer.setAttribute( 'headingRows', headingRows, modelTable );
+	}
+
+	if ( headingColumns ) {
+		conversionApi.writer.setAttribute( 'headingColumns', headingColumns, modelTable );
+	}
+
+	if ( !rows.length ) {
+		// Create empty table with one row and one table cell.
+		const row = conversionApi.writer.createElement( 'tableRow' );
+		conversionApi.writer.insert( row, ModelPosition.createAt( modelTable, 'end' ) );
+		conversionApi.writer.insertElement( 'tableCell', ModelPosition.createAt( row, 'end' ) );
+	}
+}
+
+// This one scans table rows & extracts required metadata from table:
+//
+// headingRows    - number of rows that goes as table header.
+// headingColumns - max number of row headings.
+// rows           - sorted trs as they should go into the model - ie if <thead> is inserted after <tbody> in the view.
+function _scanTable( viewTable ) {
+	const tableMeta = {
+		headingRows: 0,
+		headingColumns: 0,
+		rows: {
+			head: [],
+			body: []
+		}
+	};
+
+	let firstTheadElement;
+
+	for ( const tableChild of Array.from( viewTable.getChildren() ) ) {
+		// Only <thead>, <tbody> & <tfoot> from allowed table children can have <tr>s.
+		// The else is for future purposes (mainly <caption>).
+		if ( tableChild.name === 'tbody' || tableChild.name === 'thead' || tableChild.name === 'tfoot' ) {
+			// Parse only the first <thead> in the table as table header - all other ones will be converted to table body rows.
+			if ( tableChild.name === 'thead' && !firstTheadElement ) {
+				firstTheadElement = tableChild;
+			}
+
+			for ( const childRow of Array.from( tableChild.getChildren() ) ) {
+				_scanRow( childRow, tableMeta, firstTheadElement );
+			}
+		}
+	}
+
+	// Unify returned table meta.
+	tableMeta.rows = [ ...tableMeta.rows.head, ...tableMeta.rows.body ];
+
+	return tableMeta;
+}
+
+// Scans <tr> and it's children for metadata:
+// - For heading row:
+//     - either add this row to heading or body rows.
+//     - updates number of heading rows.
+// - For body rows:
+//     - calculates number of column headings.
+function _scanRow( tr, tableMeta, firstThead ) {
+	if ( tr.parent.name === 'thead' && tr.parent === firstThead ) {
+		// It's a table header so only update it's meta.
+		tableMeta.headingRows++;
+		tableMeta.rows.head.push( tr );
+
+		return;
+	}
+
+	// For normal row check how many column headings this row has.
+	tableMeta.rows.body.push( tr );
+
+	let headingCols = 0;
+	let index = 0;
+
+	// Filter out empty text nodes from tr children.
+	const children = Array.from( tr.getChildren() )
+		.filter( child => child.name === 'th' || child.name === 'td' );
+
+	// Count starting adjacent <th> elements of a <tr>.
+	while ( index < children.length && children[ index ].name === 'th' ) {
+		const td = children[ index ];
+
+		// Adjust columns calculation by the number of extended columns.
+		const hasAttribute = td.hasAttribute( 'colspan' );
+		const tdSize = hasAttribute ? parseInt( td.getAttribute( 'colspan' ) ) : 1;
+
+		headingCols = headingCols + tdSize;
+		index++;
+	}
+
+	if ( headingCols > tableMeta.headingColumns ) {
+		tableMeta.headingColumns = headingCols;
+	}
+}

+ 2 - 1
packages/ckeditor5-table/src/tableediting.js

@@ -9,7 +9,8 @@
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
-import { downcastTable, upcastTable } from './converters';
+import upcastTable from './converters/upcasttable';
+import downcastTable from './converters/downcasttable';
 import InsertTableCommand from './inserttablecommand';
 
 /**

+ 0 - 513
packages/ckeditor5-table/tests/converters.js

@@ -1,513 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
-import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
-import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
-
-import { setData as setModelData, getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
-import { downcastTable, upcastTable } from '../src/converters';
-
-describe( 'Table converters', () => {
-	let editor, model, viewDocument;
-
-	beforeEach( () => {
-		return VirtualTestEditor.create()
-			.then( newEditor => {
-				editor = newEditor;
-				model = editor.model;
-				viewDocument = editor.editing.view;
-
-				const conversion = editor.conversion;
-				const schema = model.schema;
-
-				schema.register( 'table', {
-					allowWhere: '$block',
-					allowAttributes: [ 'headingRows', 'headingColumns' ],
-					isBlock: true,
-					isObject: true
-				} );
-
-				schema.register( 'tableRow', {
-					allowIn: 'table',
-					allowAttributes: [],
-					isBlock: true,
-					isLimit: true
-				} );
-
-				schema.register( 'tableCell', {
-					allowIn: 'tableRow',
-					allowContentOf: '$block',
-					allowAttributes: [ 'colspan', 'rowspan' ],
-					isBlock: true,
-					isLimit: true
-				} );
-
-				conversion.for( 'upcast' ).add( upcastTable() );
-				conversion.for( 'downcast' ).add( downcastTable() );
-
-				// Table row upcast only since downcast conversion is done in `downcastTable()`.
-				conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableRow', view: 'tr' } ) );
-
-				// Table cell conversion.
-				conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) );
-				conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) );
-
-				conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
-				conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
-			} );
-	} );
-
-	describe( 'upcastTable()', () => {
-		function expectModel( data ) {
-			expect( getModelData( model, { withoutSelection: true } ) ).to.equal( data );
-		}
-
-		beforeEach( () => {
-			// Since this part of test tests only view->model conversion editing pipeline is not necessary
-			// so defining model->view converters won't be necessary.
-			editor.editing.destroy();
-		} );
-
-		it( 'should create table model from table without thead', () => {
-			editor.setData(
-				'<table>' +
-				'<tr><td>1</td></tr>' +
-				'</table>'
-			);
-
-			expectModel(
-				'<table>' +
-				'<tableRow><tableCell>1</tableCell></tableRow>' +
-				'</table>'
-			);
-		} );
-
-		it( 'should create table model from table with one thead with one row', () => {
-			editor.setData(
-				'<table>' +
-				'<thead><tr><td>1</td></tr></thead>' +
-				'</table>'
-			);
-
-			expectModel(
-				'<table headingRows="1">' +
-				'<tableRow><tableCell>1</tableCell></tableRow>' +
-				'</table>'
-			);
-		} );
-
-		it( 'should create table model from table with one thead with more then on row', () => {
-			editor.setData(
-				'<table>' +
-				'<thead>' +
-				'<tr><td>1</td></tr>' +
-				'<tr><td>2</td></tr>' +
-				'<tr><td>3</td></tr>' +
-				'</thead>' +
-				'</table>'
-			);
-
-			expectModel(
-				'<table headingRows="3">' +
-				'<tableRow><tableCell>1</tableCell></tableRow>' +
-				'<tableRow><tableCell>2</tableCell></tableRow>' +
-				'<tableRow><tableCell>3</tableCell></tableRow>' +
-				'</table>'
-			);
-		} );
-
-		it( 'should create table model from table with two theads with one row', () => {
-			editor.setData(
-				'<table>' +
-				'<thead><tr><td>1</td></tr></thead>' +
-				'<tbody><tr><td>2</td></tr></tbody>' +
-				'<thead><tr><td>3</td></tr></thead>' +
-				'</table>'
-			);
-
-			expectModel(
-				'<table headingRows="1">' +
-				'<tableRow><tableCell>1</tableCell></tableRow>' +
-				'<tableRow><tableCell>2</tableCell></tableRow>' +
-				'<tableRow><tableCell>3</tableCell></tableRow>' +
-				'</table>'
-			);
-		} );
-
-		it( 'should create table model from table with thead after the tbody', () => {
-			editor.setData(
-				'<table>' +
-				'<tbody><tr><td>2</td></tr></tbody>' +
-				'<thead><tr><td>1</td></tr></thead>' +
-				'</table>'
-			);
-
-			expectModel(
-				'<table headingRows="1">' +
-				'<tableRow><tableCell>1</tableCell></tableRow>' +
-				'<tableRow><tableCell>2</tableCell></tableRow>' +
-				'</table>'
-			);
-		} );
-
-		it( 'should create table model from table with one tfoot with one row', () => {
-			editor.setData(
-				'<table>' +
-				'<tfoot><tr><td>1</td></tr></tfoot>' +
-				'</table>'
-			);
-
-			expectModel(
-				'<table>' +
-				'<tableRow><tableCell>1</tableCell></tableRow>' +
-				'</table>'
-			);
-		} );
-
-		it( 'should create valid table model from empty table', () => {
-			editor.setData(
-				'<table>' +
-				'</table>'
-			);
-
-			expectModel(
-				'<table><tableRow><tableCell></tableCell></tableRow></table>'
-			);
-		} );
-
-		it( 'should skip unknown table children', () => {
-			editor.setData(
-				'<table>' +
-				'<caption>foo</caption>' +
-				'<tr><td>bar</td></tr>' +
-				'</table>'
-			);
-
-			expectModel(
-				'<table><tableRow><tableCell>bar</tableCell></tableRow></table>'
-			);
-		} );
-
-		it( 'should create table model from some broken table', () => {
-			editor.setData(
-				'<table><td><p>foo</p></td></table>'
-			);
-
-			expectModel(
-				'<table><tableRow><tableCell>foo</tableCell></tableRow></table>'
-			);
-		} );
-
-		it( 'should fix if inside other blocks', () => {
-			// Using <div> instead of <p> as it breaks on Edge.
-			editor.model.schema.register( 'div', {
-				inheritAllFrom: '$block'
-			} );
-			editor.conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'div', view: 'div' } ) );
-
-			editor.setData(
-				'<div>foo' +
-				'<table>' +
-				'<tbody><tr><td>2</td></tr></tbody>' +
-				'<thead><tr><td>1</td></tr></thead>' +
-				'</table>' +
-				'bar</div>'
-			);
-
-			expectModel(
-				'<div>foo</div>' +
-				'<table headingRows="1">' +
-				'<tableRow><tableCell>1</tableCell></tableRow>' +
-				'<tableRow><tableCell>2</tableCell></tableRow>' +
-				'</table>' +
-				'<div>bar</div>'
-			);
-		} );
-
-		it( 'should be possible to overwrite table conversion', () => {
-			editor.model.schema.register( 'fooTable', {
-				allowWhere: '$block',
-				allowAttributes: [ 'headingRows' ],
-				isBlock: true,
-				isObject: true
-			} );
-
-			editor.conversion.elementToElement( { model: 'fooTable', view: 'table', priority: 'high' } );
-
-			editor.setData(
-				'<table>' +
-				'<thead><tr><td>foo</td></tr></thead>' +
-				'</table>'
-			);
-
-			expectModel(
-				'<fooTable></fooTable>'
-			);
-		} );
-
-		describe( 'headingColumns', () => {
-			it( 'should properly calculate heading columns', () => {
-				editor.setData(
-					'<table>' +
-					'<tbody>' +
-					// This row starts with 1 th (3 total).
-					'<tr><th>21</th><td>22</td><th>23</th><th>24</th></tr>' +
-					// This row starts with 2 th (2 total). This one has max number of heading columns: 2.
-					'<tr><th>31</th><th>32</th><td>33</td><td>34</td></tr>' +
-					// This row starts with 1 th (1 total).
-					'<tr><th>41</th><td>42</td><td>43</td><td>44</td></tr>' +
-					// This row starts with 0 th (3 total).
-					'<tr><td>51</td><th>52</th><th>53</th><th>54</th></tr>' +
-					'</tbody>' +
-					'<thead>' +
-					// This row has 4 ths but it is a thead.
-					'<tr><th>11</th><th>12</th><th>13</th><th>14</th></tr>' +
-					'</thead>' +
-					'</table>'
-				);
-
-				expectModel(
-					'<table headingColumns="2" headingRows="1">' +
-					'<tableRow>' +
-					'<tableCell>11</tableCell><tableCell>12</tableCell><tableCell>13</tableCell><tableCell>14</tableCell>' +
-					'</tableRow>' +
-					'<tableRow>' +
-					'<tableCell>21</tableCell><tableCell>22</tableCell><tableCell>23</tableCell><tableCell>24</tableCell>' +
-					'</tableRow>' +
-					'<tableRow>' +
-					'<tableCell>31</tableCell><tableCell>32</tableCell><tableCell>33</tableCell><tableCell>34</tableCell>' +
-					'</tableRow>' +
-					'<tableRow>' +
-					'<tableCell>41</tableCell><tableCell>42</tableCell><tableCell>43</tableCell><tableCell>44</tableCell>' +
-					'</tableRow>' +
-					'<tableRow>' +
-					'<tableCell>51</tableCell><tableCell>52</tableCell><tableCell>53</tableCell><tableCell>54</tableCell>' +
-					'</tableRow>' +
-					'</table>'
-				);
-			} );
-
-			it( 'should calculate heading columns of cells with colspan', () => {
-				editor.setData(
-					'<table>' +
-					'<tbody>' +
-					// This row has colspan of 3 so it should be the whole table should have 3 heading columns.
-					'<tr><th>21</th><th>22</th><td>23</td><td>24</td></tr>' +
-					'<tr><th colspan="2">31</th><th>33</th><td>34</td></tr>' +
-					'</tbody>' +
-					'<thead>' +
-					// This row has 4 ths but it is a thead.
-					'<tr><th>11</th><th>12</th><th>13</th><th>14</th></tr>' +
-					'</thead>' +
-					'</table>'
-				);
-
-				expectModel(
-					'<table headingColumns="3" headingRows="1">' +
-					'<tableRow>' +
-					'<tableCell>11</tableCell><tableCell>12</tableCell><tableCell>13</tableCell><tableCell>14</tableCell>' +
-					'</tableRow>' +
-					'<tableRow>' +
-					'<tableCell>21</tableCell><tableCell>22</tableCell><tableCell>23</tableCell><tableCell>24</tableCell>' +
-					'</tableRow>' +
-					'<tableRow>' +
-					'<tableCell colspan="2">31</tableCell><tableCell>33</tableCell><tableCell>34</tableCell>' +
-					'</tableRow>' +
-					'</table>'
-				);
-			} );
-		} );
-	} );
-
-	describe( 'downcastTable()', () => {
-		it( 'should create table with tbody', () => {
-			setModelData( model,
-				'<table>' +
-				'<tableRow><tableCell></tableCell></tableRow>' +
-				'</table>'
-			);
-
-			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-				'<table>' +
-				'<tbody>' +
-				'<tr><td></td></tr>' +
-				'</tbody>' +
-				'</table>'
-			);
-		} );
-
-		it( 'should create table with tbody and thead', () => {
-			setModelData( model,
-				'<table headingRows="1">' +
-				'<tableRow><tableCell>1</tableCell></tableRow>' +
-				'<tableRow><tableCell>2</tableCell></tableRow>' +
-				'</table>'
-			);
-
-			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-				'<table>' +
-				'<thead>' +
-				'<tr><th>1</th></tr>' +
-				'</thead>' +
-				'<tbody>' +
-				'<tr><td>2</td></tr>' +
-				'</tbody>' +
-				'</table>'
-			);
-		} );
-
-		it( 'should create table with thead', () => {
-			setModelData( model,
-				'<table headingRows="2">' +
-				'<tableRow><tableCell>1</tableCell></tableRow>' +
-				'<tableRow><tableCell>2</tableCell></tableRow>' +
-				'</table>'
-			);
-
-			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-				'<table>' +
-				'<thead>' +
-				'<tr><th>1</th></tr>' +
-				'<tr><th>2</th></tr>' +
-				'</thead>' +
-				'</table>'
-			);
-		} );
-
-		it( 'should create table with heading columns and rows', () => {
-			setModelData( model,
-				'<table headingColumns="3" headingRows="1">' +
-				'<tableRow>' +
-				'<tableCell>11</tableCell><tableCell>12</tableCell><tableCell>13</tableCell><tableCell>14</tableCell>' +
-				'</tableRow>' +
-				'<tableRow>' +
-				'<tableCell>21</tableCell><tableCell>22</tableCell><tableCell>23</tableCell><tableCell>24</tableCell>' +
-				'</tableRow>' +
-				'</table>'
-			);
-
-			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-				'<table>' +
-				'<thead>' +
-				'<tr><th>11</th><th>12</th><th>13</th><th>14</th></tr>' +
-				'</thead>' +
-				'<tbody>' +
-				'<tr><th>21</th><th>22</th><th>23</th><td>24</td></tr>' +
-				'</tbody>' +
-				'</table>'
-			);
-		} );
-
-		it( 'should be possible to overwrite', () => {
-			editor.conversion.elementToElement( { model: 'tableRow', view: 'tr' } );
-			editor.conversion.elementToElement( { model: 'tableCell', view: 'td' } );
-			editor.conversion.for( 'downcast' ).add( dispatcher => {
-				dispatcher.on( 'insert:table', ( evt, data, conversionApi ) => {
-					conversionApi.consumable.consume( data.item, 'insert' );
-
-					const tableElement = conversionApi.writer.createContainerElement( 'table', { foo: 'bar' } );
-					const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
-
-					conversionApi.mapper.bindElements( data.item, tableElement );
-					conversionApi.writer.insert( viewPosition, tableElement );
-				}, { priority: 'high' } );
-			} );
-
-			setModelData( model,
-				'<table>' +
-				'<tableRow><tableCell></tableCell></tableRow>' +
-				'</table>'
-			);
-
-			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-				'<table foo="bar">' +
-				'<tr><td></td></tr>' +
-				'</table>'
-			);
-		} );
-
-		describe( 'headingColumns attribute', () => {
-			it( 'should mark heading columns table cells', () => {
-				setModelData( model,
-					'<table headingColumns="2">' +
-					'<tableRow><tableCell>11</tableCell><tableCell>12</tableCell><tableCell>13</tableCell></tableRow>' +
-					'<tableRow><tableCell>21</tableCell><tableCell>22</tableCell><tableCell>23</tableCell></tableRow>' +
-					'</table>'
-				);
-
-				expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-					'<table>' +
-					'<tbody>' +
-					'<tr><th>11</th><th>12</th><td>13</td></tr>' +
-					'<tr><th>21</th><th>22</th><td>23</td></tr>' +
-					'</tbody>' +
-					'</table>'
-				);
-			} );
-
-			it( 'should mark heading columns table cells when one has colspan attribute', () => {
-				setModelData( model,
-					'<table headingColumns="3">' +
-					'<tableRow>' +
-					'<tableCell>11</tableCell><tableCell>12</tableCell><tableCell>13</tableCell><tableCell>14</tableCell>' +
-					'</tableRow>' +
-					'<tableRow><tableCell colspan="2">21</tableCell><tableCell>23</tableCell><tableCell>24</tableCell></tableRow>' +
-					'</table>'
-				);
-
-				expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-					'<table>' +
-					'<tbody>' +
-					'<tr><th>11</th><th>12</th><th>13</th><td>14</td></tr>' +
-					'<tr><th colspan="2">21</th><th>23</th><td>24</td></tr>' +
-					'</tbody>' +
-					'</table>'
-				);
-			} );
-
-			it( 'should work with colspan and rowspan attributes on table cells', () => {
-				// The table in this test looks like a table below:
-				//
-				//   Row headings | Normal cells
-				//                |
-				// +----+----+----+----+
-				// | 11 | 12 | 13 | 14 |
-				// |    +----+    +----+
-				// |    | 22 |    | 24 |
-				// |----+----+    +----+
-				// | 31      |    | 34 |
-				// |         +----+----+
-				// |         | 43 | 44 |
-				// +----+----+----+----+
-
-				setModelData( model,
-					'<table headingColumns="3">' +
-					'<tableRow>' +
-					'<tableCell rowspan="2">11</tableCell>' +
-					'<tableCell>12</tableCell>' +
-					'<tableCell rowspan="3">13</tableCell>' +
-					'<tableCell>14</tableCell>' +
-					'</tableRow>' +
-					'<tableRow><tableCell>22</tableCell><tableCell>24</tableCell></tableRow>' +
-					'<tableRow><tableCell colspan="2" rowspan="2">31</tableCell><tableCell>34</tableCell></tableRow>' +
-					'<tableRow><tableCell>43</tableCell><tableCell>44</tableCell></tableRow>' +
-					'</table>'
-				);
-
-				expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-					'<table>' +
-					'<tbody>' +
-					'<tr><th rowspan="2">11</th><th>12</th><th rowspan="3">13</th><td>14</td></tr>' +
-					'<tr><th>22</th><td>24</td></tr>' +
-					'<tr><th colspan="2" rowspan="2">31</th><td>34</td></tr>' +
-					'<tr><th>43</th><td>44</td></tr>' +
-					'</tbody>' +
-					'</table>'
-				);
-			} );
-		} );
-	} );
-} );

+ 239 - 0
packages/ckeditor5-table/tests/converters/downcasttable.js

@@ -0,0 +1,239 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
+
+import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import downcastTable from '../../src/converters/downcasttable';
+
+describe( 'downcastTable()', () => {
+	let editor, model, viewDocument;
+
+	beforeEach( () => {
+		return VirtualTestEditor.create()
+			.then( newEditor => {
+				editor = newEditor;
+				model = editor.model;
+				viewDocument = editor.editing.view;
+
+				const conversion = editor.conversion;
+				const schema = model.schema;
+
+				schema.register( 'table', {
+					allowWhere: '$block',
+					allowAttributes: [ 'headingRows', 'headingColumns' ],
+					isBlock: true,
+					isObject: true
+				} );
+
+				schema.register( 'tableRow', {
+					allowIn: 'table',
+					allowAttributes: [],
+					isBlock: true,
+					isLimit: true
+				} );
+
+				schema.register( 'tableCell', {
+					allowIn: 'tableRow',
+					allowContentOf: '$block',
+					allowAttributes: [ 'colspan', 'rowspan' ],
+					isBlock: true,
+					isLimit: true
+				} );
+
+				conversion.for( 'downcast' ).add( downcastTable() );
+				conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
+				conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
+			} );
+	} );
+
+	it( 'should create table with tbody', () => {
+		setModelData( model,
+			'<table>' +
+			'<tableRow><tableCell></tableCell></tableRow>' +
+			'</table>'
+		);
+
+		expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+			'<table>' +
+			'<tbody>' +
+			'<tr><td></td></tr>' +
+			'</tbody>' +
+			'</table>'
+		);
+	} );
+
+	it( 'should create table with tbody and thead', () => {
+		setModelData( model,
+			'<table headingRows="1">' +
+			'<tableRow><tableCell>1</tableCell></tableRow>' +
+			'<tableRow><tableCell>2</tableCell></tableRow>' +
+			'</table>'
+		);
+
+		expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+			'<table>' +
+			'<thead>' +
+			'<tr><th>1</th></tr>' +
+			'</thead>' +
+			'<tbody>' +
+			'<tr><td>2</td></tr>' +
+			'</tbody>' +
+			'</table>'
+		);
+	} );
+
+	it( 'should create table with thead', () => {
+		setModelData( model,
+			'<table headingRows="2">' +
+			'<tableRow><tableCell>1</tableCell></tableRow>' +
+			'<tableRow><tableCell>2</tableCell></tableRow>' +
+			'</table>'
+		);
+
+		expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+			'<table>' +
+			'<thead>' +
+			'<tr><th>1</th></tr>' +
+			'<tr><th>2</th></tr>' +
+			'</thead>' +
+			'</table>'
+		);
+	} );
+
+	it( 'should create table with heading columns and rows', () => {
+		setModelData( model,
+			'<table headingColumns="3" headingRows="1">' +
+			'<tableRow>' +
+			'<tableCell>11</tableCell><tableCell>12</tableCell><tableCell>13</tableCell><tableCell>14</tableCell>' +
+			'</tableRow>' +
+			'<tableRow>' +
+			'<tableCell>21</tableCell><tableCell>22</tableCell><tableCell>23</tableCell><tableCell>24</tableCell>' +
+			'</tableRow>' +
+			'</table>'
+		);
+
+		expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+			'<table>' +
+			'<thead>' +
+			'<tr><th>11</th><th>12</th><th>13</th><th>14</th></tr>' +
+			'</thead>' +
+			'<tbody>' +
+			'<tr><th>21</th><th>22</th><th>23</th><td>24</td></tr>' +
+			'</tbody>' +
+			'</table>'
+		);
+	} );
+
+	it( 'should be possible to overwrite', () => {
+		editor.conversion.elementToElement( { model: 'tableRow', view: 'tr' } );
+		editor.conversion.elementToElement( { model: 'tableCell', view: 'td' } );
+		editor.conversion.for( 'downcast' ).add( dispatcher => {
+			dispatcher.on( 'insert:table', ( evt, data, conversionApi ) => {
+				conversionApi.consumable.consume( data.item, 'insert' );
+
+				const tableElement = conversionApi.writer.createContainerElement( 'table', { foo: 'bar' } );
+				const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
+
+				conversionApi.mapper.bindElements( data.item, tableElement );
+				conversionApi.writer.insert( viewPosition, tableElement );
+			}, { priority: 'high' } );
+		} );
+
+		setModelData( model,
+			'<table>' +
+			'<tableRow><tableCell></tableCell></tableRow>' +
+			'</table>'
+		);
+
+		expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+			'<table foo="bar">' +
+			'<tr><td></td></tr>' +
+			'</table>'
+		);
+	} );
+
+	describe( 'headingColumns attribute', () => {
+		it( 'should mark heading columns table cells', () => {
+			setModelData( model,
+				'<table headingColumns="2">' +
+				'<tableRow><tableCell>11</tableCell><tableCell>12</tableCell><tableCell>13</tableCell></tableRow>' +
+				'<tableRow><tableCell>21</tableCell><tableCell>22</tableCell><tableCell>23</tableCell></tableRow>' +
+				'</table>'
+			);
+
+			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+				'<table>' +
+				'<tbody>' +
+				'<tr><th>11</th><th>12</th><td>13</td></tr>' +
+				'<tr><th>21</th><th>22</th><td>23</td></tr>' +
+				'</tbody>' +
+				'</table>'
+			);
+		} );
+
+		it( 'should mark heading columns table cells when one has colspan attribute', () => {
+			setModelData( model,
+				'<table headingColumns="3">' +
+				'<tableRow>' +
+				'<tableCell>11</tableCell><tableCell>12</tableCell><tableCell>13</tableCell><tableCell>14</tableCell>' +
+				'</tableRow>' +
+				'<tableRow><tableCell colspan="2">21</tableCell><tableCell>23</tableCell><tableCell>24</tableCell></tableRow>' +
+				'</table>'
+			);
+
+			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+				'<table>' +
+				'<tbody>' +
+				'<tr><th>11</th><th>12</th><th>13</th><td>14</td></tr>' +
+				'<tr><th colspan="2">21</th><th>23</th><td>24</td></tr>' +
+				'</tbody>' +
+				'</table>'
+			);
+		} );
+
+		it( 'should work with colspan and rowspan attributes on table cells', () => {
+			// The table in this test looks like a table below:
+			//
+			//   Row headings | Normal cells
+			//                |
+			// +----+----+----+----+
+			// | 11 | 12 | 13 | 14 |
+			// |    +----+    +----+
+			// |    | 22 |    | 24 |
+			// |----+----+    +----+
+			// | 31      |    | 34 |
+			// |         +----+----+
+			// |         | 43 | 44 |
+			// +----+----+----+----+
+
+			setModelData( model,
+				'<table headingColumns="3">' +
+				'<tableRow>' +
+				'<tableCell rowspan="2">11</tableCell>' +
+				'<tableCell>12</tableCell>' +
+				'<tableCell rowspan="3">13</tableCell>' +
+				'<tableCell>14</tableCell>' +
+				'</tableRow>' +
+				'<tableRow><tableCell>22</tableCell><tableCell>24</tableCell></tableRow>' +
+				'<tableRow><tableCell colspan="2" rowspan="2">31</tableCell><tableCell>34</tableCell></tableRow>' +
+				'<tableRow><tableCell>43</tableCell><tableCell>44</tableCell></tableRow>' +
+				'</table>'
+			);
+
+			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+				'<table>' +
+				'<tbody>' +
+				'<tr><th rowspan="2">11</th><th>12</th><th rowspan="3">13</th><td>14</td></tr>' +
+				'<tr><th>22</th><td>24</td></tr>' +
+				'<tr><th colspan="2" rowspan="2">31</th><td>34</td></tr>' +
+				'<tr><th>43</th><td>44</td></tr>' +
+				'</tbody>' +
+				'</table>'
+			);
+		} );
+	} );
+} );

+ 317 - 0
packages/ckeditor5-table/tests/converters/upcasttable.js

@@ -0,0 +1,317 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
+
+import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import upcastTable from '../../src/converters/upcasttable';
+
+describe( 'upcastTable()', () => {
+	let editor, model;
+
+	beforeEach( () => {
+		return VirtualTestEditor.create()
+			.then( newEditor => {
+				editor = newEditor;
+				model = editor.model;
+
+				const conversion = editor.conversion;
+				const schema = model.schema;
+
+				schema.register( 'table', {
+					allowWhere: '$block',
+					allowAttributes: [ 'headingRows', 'headingColumns' ],
+					isBlock: true,
+					isObject: true
+				} );
+
+				schema.register( 'tableRow', {
+					allowIn: 'table',
+					allowAttributes: [],
+					isBlock: true,
+					isLimit: true
+				} );
+
+				schema.register( 'tableCell', {
+					allowIn: 'tableRow',
+					allowContentOf: '$block',
+					allowAttributes: [ 'colspan', 'rowspan' ],
+					isBlock: true,
+					isLimit: true
+				} );
+
+				conversion.for( 'upcast' ).add( upcastTable() );
+
+				// Table row upcast only since downcast conversion is done in `downcastTable()`.
+				conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableRow', view: 'tr' } ) );
+
+				// Table cell conversion.
+				conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) );
+				conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) );
+
+				conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
+				conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
+
+				// Since this part of test tests only view->model conversion editing pipeline is not necessary
+				// so defining model->view converters won't be necessary.
+				editor.editing.destroy();
+			} );
+	} );
+
+	function expectModel( data ) {
+		expect( getModelData( model, { withoutSelection: true } ) ).to.equal( data );
+	}
+
+	it( 'should create table model from table without thead', () => {
+		editor.setData(
+			'<table>' +
+			'<tr><td>1</td></tr>' +
+			'</table>'
+		);
+
+		expectModel(
+			'<table>' +
+			'<tableRow><tableCell>1</tableCell></tableRow>' +
+			'</table>'
+		);
+	} );
+
+	it( 'should create table model from table with one thead with one row', () => {
+		editor.setData(
+			'<table>' +
+			'<thead><tr><td>1</td></tr></thead>' +
+			'</table>'
+		);
+
+		expectModel(
+			'<table headingRows="1">' +
+			'<tableRow><tableCell>1</tableCell></tableRow>' +
+			'</table>'
+		);
+	} );
+
+	it( 'should create table model from table with one thead with more then on row', () => {
+		editor.setData(
+			'<table>' +
+			'<thead>' +
+			'<tr><td>1</td></tr>' +
+			'<tr><td>2</td></tr>' +
+			'<tr><td>3</td></tr>' +
+			'</thead>' +
+			'</table>'
+		);
+
+		expectModel(
+			'<table headingRows="3">' +
+			'<tableRow><tableCell>1</tableCell></tableRow>' +
+			'<tableRow><tableCell>2</tableCell></tableRow>' +
+			'<tableRow><tableCell>3</tableCell></tableRow>' +
+			'</table>'
+		);
+	} );
+
+	it( 'should create table model from table with two theads with one row', () => {
+		editor.setData(
+			'<table>' +
+			'<thead><tr><td>1</td></tr></thead>' +
+			'<tbody><tr><td>2</td></tr></tbody>' +
+			'<thead><tr><td>3</td></tr></thead>' +
+			'</table>'
+		);
+
+		expectModel(
+			'<table headingRows="1">' +
+			'<tableRow><tableCell>1</tableCell></tableRow>' +
+			'<tableRow><tableCell>2</tableCell></tableRow>' +
+			'<tableRow><tableCell>3</tableCell></tableRow>' +
+			'</table>'
+		);
+	} );
+
+	it( 'should create table model from table with thead after the tbody', () => {
+		editor.setData(
+			'<table>' +
+			'<tbody><tr><td>2</td></tr></tbody>' +
+			'<thead><tr><td>1</td></tr></thead>' +
+			'</table>'
+		);
+
+		expectModel(
+			'<table headingRows="1">' +
+			'<tableRow><tableCell>1</tableCell></tableRow>' +
+			'<tableRow><tableCell>2</tableCell></tableRow>' +
+			'</table>'
+		);
+	} );
+
+	it( 'should create table model from table with one tfoot with one row', () => {
+		editor.setData(
+			'<table>' +
+			'<tfoot><tr><td>1</td></tr></tfoot>' +
+			'</table>'
+		);
+
+		expectModel(
+			'<table>' +
+			'<tableRow><tableCell>1</tableCell></tableRow>' +
+			'</table>'
+		);
+	} );
+
+	it( 'should create valid table model from empty table', () => {
+		editor.setData(
+			'<table>' +
+			'</table>'
+		);
+
+		expectModel(
+			'<table><tableRow><tableCell></tableCell></tableRow></table>'
+		);
+	} );
+
+	it( 'should skip unknown table children', () => {
+		editor.setData(
+			'<table>' +
+			'<caption>foo</caption>' +
+			'<tr><td>bar</td></tr>' +
+			'</table>'
+		);
+
+		expectModel(
+			'<table><tableRow><tableCell>bar</tableCell></tableRow></table>'
+		);
+	} );
+
+	it( 'should create table model from some broken table', () => {
+		editor.setData(
+			'<table><td><p>foo</p></td></table>'
+		);
+
+		expectModel(
+			'<table><tableRow><tableCell>foo</tableCell></tableRow></table>'
+		);
+	} );
+
+	it( 'should fix if inside other blocks', () => {
+		// Using <div> instead of <p> as it breaks on Edge.
+		editor.model.schema.register( 'div', {
+			inheritAllFrom: '$block'
+		} );
+		editor.conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'div', view: 'div' } ) );
+
+		editor.setData(
+			'<div>foo' +
+			'<table>' +
+			'<tbody><tr><td>2</td></tr></tbody>' +
+			'<thead><tr><td>1</td></tr></thead>' +
+			'</table>' +
+			'bar</div>'
+		);
+
+		expectModel(
+			'<div>foo</div>' +
+			'<table headingRows="1">' +
+			'<tableRow><tableCell>1</tableCell></tableRow>' +
+			'<tableRow><tableCell>2</tableCell></tableRow>' +
+			'</table>' +
+			'<div>bar</div>'
+		);
+	} );
+
+	it( 'should be possible to overwrite table conversion', () => {
+		editor.model.schema.register( 'fooTable', {
+			allowWhere: '$block',
+			allowAttributes: [ 'headingRows' ],
+			isBlock: true,
+			isObject: true
+		} );
+
+		editor.conversion.elementToElement( { model: 'fooTable', view: 'table', priority: 'high' } );
+
+		editor.setData(
+			'<table>' +
+			'<thead><tr><td>foo</td></tr></thead>' +
+			'</table>'
+		);
+
+		expectModel(
+			'<fooTable></fooTable>'
+		);
+	} );
+
+	describe( 'headingColumns', () => {
+		it( 'should properly calculate heading columns', () => {
+			editor.setData(
+				'<table>' +
+				'<tbody>' +
+				// This row starts with 1 th (3 total).
+				'<tr><th>21</th><td>22</td><th>23</th><th>24</th></tr>' +
+				// This row starts with 2 th (2 total). This one has max number of heading columns: 2.
+				'<tr><th>31</th><th>32</th><td>33</td><td>34</td></tr>' +
+				// This row starts with 1 th (1 total).
+				'<tr><th>41</th><td>42</td><td>43</td><td>44</td></tr>' +
+				// This row starts with 0 th (3 total).
+				'<tr><td>51</td><th>52</th><th>53</th><th>54</th></tr>' +
+				'</tbody>' +
+				'<thead>' +
+				// This row has 4 ths but it is a thead.
+				'<tr><th>11</th><th>12</th><th>13</th><th>14</th></tr>' +
+				'</thead>' +
+				'</table>'
+			);
+
+			expectModel(
+				'<table headingColumns="2" headingRows="1">' +
+				'<tableRow>' +
+				'<tableCell>11</tableCell><tableCell>12</tableCell><tableCell>13</tableCell><tableCell>14</tableCell>' +
+				'</tableRow>' +
+				'<tableRow>' +
+				'<tableCell>21</tableCell><tableCell>22</tableCell><tableCell>23</tableCell><tableCell>24</tableCell>' +
+				'</tableRow>' +
+				'<tableRow>' +
+				'<tableCell>31</tableCell><tableCell>32</tableCell><tableCell>33</tableCell><tableCell>34</tableCell>' +
+				'</tableRow>' +
+				'<tableRow>' +
+				'<tableCell>41</tableCell><tableCell>42</tableCell><tableCell>43</tableCell><tableCell>44</tableCell>' +
+				'</tableRow>' +
+				'<tableRow>' +
+				'<tableCell>51</tableCell><tableCell>52</tableCell><tableCell>53</tableCell><tableCell>54</tableCell>' +
+				'</tableRow>' +
+				'</table>'
+			);
+		} );
+
+		it( 'should calculate heading columns of cells with colspan', () => {
+			editor.setData(
+				'<table>' +
+				'<tbody>' +
+				// This row has colspan of 3 so it should be the whole table should have 3 heading columns.
+				'<tr><th>21</th><th>22</th><td>23</td><td>24</td></tr>' +
+				'<tr><th colspan="2">31</th><th>33</th><td>34</td></tr>' +
+				'</tbody>' +
+				'<thead>' +
+				// This row has 4 ths but it is a thead.
+				'<tr><th>11</th><th>12</th><th>13</th><th>14</th></tr>' +
+				'</thead>' +
+				'</table>'
+			);
+
+			expectModel(
+				'<table headingColumns="3" headingRows="1">' +
+				'<tableRow>' +
+				'<tableCell>11</tableCell><tableCell>12</tableCell><tableCell>13</tableCell><tableCell>14</tableCell>' +
+				'</tableRow>' +
+				'<tableRow>' +
+				'<tableCell>21</tableCell><tableCell>22</tableCell><tableCell>23</tableCell><tableCell>24</tableCell>' +
+				'</tableRow>' +
+				'<tableRow>' +
+				'<tableCell colspan="2">31</tableCell><tableCell>33</tableCell><tableCell>34</tableCell>' +
+				'</tableRow>' +
+				'</table>'
+			);
+		} );
+	} );
+} );

+ 2 - 1
packages/ckeditor5-table/tests/inserttablecommand.js

@@ -8,7 +8,8 @@ import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model
 import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
 
 import InsertTableCommand from '../src/inserttablecommand';
-import { upcastTable, downcastTable } from '../src/converters';
+import downcastTable from '../src/converters/downcasttable';
+import upcastTable from '../src/converters/upcasttable';
 
 describe( 'InsertTableCommand', () => {
 	let editor, model, command;