mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-09-27 20:26:46 +00:00
d70aa70707
nesting validity isn't checked yet (lists inside lists are illegal IIRC), but for now it tests the reversal of the order of the closing tags nicely
568 lines
16 KiB
JavaScript
568 lines
16 KiB
JavaScript
module( 've/dm' );
|
|
|
|
test( 've.dm.TransactionProcessor', 39, function() {
|
|
var documentModel = ve.dm.DocumentNode.newFromPlainObject( veTest.obj );
|
|
|
|
// FIXME: These tests shouldn't use prepareFoo() because those functions
|
|
// normalize the transactions they create and are tested separately.
|
|
// We should be creating transactions directly and feeding those into
|
|
// commit()/rollback() --Roan
|
|
var elementAttributeChange = documentModel.prepareElementAttributeChange(
|
|
0, 'test', 1
|
|
);
|
|
|
|
// Test 1
|
|
ve.dm.TransactionProcessor.commit( documentModel, elementAttributeChange );
|
|
deepEqual(
|
|
documentModel.getData( new ve.Range( 0, 5 ) ),
|
|
[
|
|
{ 'type': 'paragraph', 'attributes': { 'test': 1 } },
|
|
'a',
|
|
['b', { 'type': 'textStyle/bold', 'hash': '{"type":"textStyle/bold"}' }],
|
|
['c', { 'type': 'textStyle/italic', 'hash': '{"type":"textStyle/italic"}' }],
|
|
{ 'type': '/paragraph' }
|
|
],
|
|
'commit applies an element attribute change transaction to the content'
|
|
);
|
|
|
|
// Test 2
|
|
ve.dm.TransactionProcessor.rollback( documentModel, elementAttributeChange );
|
|
deepEqual(
|
|
documentModel.getData( new ve.Range( 0, 5 ) ),
|
|
[
|
|
{ 'type': 'paragraph' },
|
|
'a',
|
|
['b', { 'type': 'textStyle/bold', 'hash': '{"type":"textStyle/bold"}' }],
|
|
['c', { 'type': 'textStyle/italic', 'hash': '{"type":"textStyle/italic"}' }],
|
|
{ 'type': '/paragraph' }
|
|
],
|
|
'rollback reverses the effect of an element attribute change transaction on the content'
|
|
);
|
|
|
|
var contentAnnotation = documentModel.prepareContentAnnotation(
|
|
new ve.Range( 1, 4 ), 'set', { 'type': 'textStyle/bold' }
|
|
);
|
|
|
|
// Test 3
|
|
ve.dm.TransactionProcessor.commit( documentModel, contentAnnotation );
|
|
deepEqual(
|
|
documentModel.getData( new ve.Range( 0, 5 ) ),
|
|
[
|
|
{ 'type': 'paragraph' },
|
|
['a', { 'type': 'textStyle/bold', 'hash': '{"type":"textStyle/bold"}' }],
|
|
['b', { 'type': 'textStyle/bold', 'hash': '{"type":"textStyle/bold"}' }],
|
|
[
|
|
'c',
|
|
{ 'type': 'textStyle/italic', 'hash': '{"type":"textStyle/italic"}' },
|
|
{ 'type': 'textStyle/bold', 'hash': '{"type":"textStyle/bold"}' }
|
|
],
|
|
{ 'type': '/paragraph' }
|
|
],
|
|
'commit applies a content annotation transaction to the content'
|
|
);
|
|
|
|
// Test 4
|
|
ve.dm.TransactionProcessor.rollback( documentModel, contentAnnotation );
|
|
deepEqual(
|
|
documentModel.getData( new ve.Range( 0, 5 ) ),
|
|
[
|
|
{ 'type': 'paragraph' },
|
|
'a',
|
|
['b', { 'type': 'textStyle/bold', 'hash': '{"type":"textStyle/bold"}' }],
|
|
['c', { 'type': 'textStyle/italic', 'hash': '{"type":"textStyle/italic"}' }],
|
|
{ 'type': '/paragraph' }
|
|
],
|
|
'rollback reverses the effect of a content annotation transaction on the content'
|
|
);
|
|
|
|
var insertion = documentModel.prepareInsertion( 3, ['d'] );
|
|
|
|
// Test 5
|
|
ve.dm.TransactionProcessor.commit( documentModel, insertion );
|
|
deepEqual(
|
|
documentModel.getData( new ve.Range( 0, 6 ) ),
|
|
[
|
|
{ 'type': 'paragraph' },
|
|
'a',
|
|
['b', { 'type': 'textStyle/bold', 'hash': '{"type":"textStyle/bold"}' }],
|
|
'd',
|
|
['c', { 'type': 'textStyle/italic', 'hash': '{"type":"textStyle/italic"}' }],
|
|
{ 'type': '/paragraph' }
|
|
],
|
|
'commit applies an insertion transaction to the content'
|
|
);
|
|
|
|
// Test 6
|
|
deepEqual(
|
|
documentModel.getChildren()[0].getContentData(),
|
|
[
|
|
'a',
|
|
['b', { 'type': 'textStyle/bold', 'hash': '{"type":"textStyle/bold"}' }],
|
|
'd',
|
|
['c', { 'type': 'textStyle/italic', 'hash': '{"type":"textStyle/italic"}' }]
|
|
],
|
|
'commit keeps model tree up to date with insertions'
|
|
);
|
|
|
|
// Test 7
|
|
ve.dm.TransactionProcessor.rollback( documentModel, insertion );
|
|
deepEqual(
|
|
documentModel.getData( new ve.Range( 0, 5 ) ),
|
|
[
|
|
{ 'type': 'paragraph' },
|
|
'a',
|
|
['b', { 'type': 'textStyle/bold', 'hash': '{"type":"textStyle/bold"}' }],
|
|
['c', { 'type': 'textStyle/italic', 'hash': '{"type":"textStyle/italic"}' }],
|
|
{ 'type': '/paragraph' }
|
|
],
|
|
'rollback reverses the effect of an insertion transaction on the content'
|
|
);
|
|
|
|
// Test 8
|
|
deepEqual(
|
|
documentModel.getChildren()[0].getContentData(),
|
|
[
|
|
'a',
|
|
['b', { 'type': 'textStyle/bold', 'hash': '{"type":"textStyle/bold"}' }],
|
|
['c', { 'type': 'textStyle/italic', 'hash': '{"type":"textStyle/italic"}' }]
|
|
],
|
|
'rollback keeps model tree up to date with insertions'
|
|
);
|
|
|
|
var removal = documentModel.prepareRemoval( new ve.Range( 2, 4 ) );
|
|
|
|
// Test 9
|
|
ve.dm.TransactionProcessor.commit( documentModel, removal );
|
|
deepEqual(
|
|
documentModel.getData( new ve.Range( 0, 3 ) ),
|
|
[
|
|
{ 'type': 'paragraph' },
|
|
'a',
|
|
{ 'type': '/paragraph' }
|
|
],
|
|
'commit applies a removal transaction to the content'
|
|
);
|
|
|
|
// Test 10
|
|
deepEqual(
|
|
documentModel.getChildren()[0].getContentData(),
|
|
['a'],
|
|
'commit keeps model tree up to date with removals'
|
|
);
|
|
|
|
// Test 11
|
|
ve.dm.TransactionProcessor.rollback( documentModel, removal );
|
|
deepEqual(
|
|
documentModel.getData( new ve.Range( 0, 5 ) ),
|
|
[
|
|
{ 'type': 'paragraph' },
|
|
'a',
|
|
['b', { 'type': 'textStyle/bold', 'hash': '{"type":"textStyle/bold"}' }],
|
|
['c', { 'type': 'textStyle/italic', 'hash': '{"type":"textStyle/italic"}' }],
|
|
{ 'type': '/paragraph' }
|
|
],
|
|
'rollback reverses the effect of a removal transaction on the content'
|
|
);
|
|
|
|
// Test 12
|
|
deepEqual(
|
|
documentModel.getChildren()[0].getContentData(),
|
|
[
|
|
'a',
|
|
['b', { 'type': 'textStyle/bold', 'hash': '{"type":"textStyle/bold"}' }],
|
|
['c', { 'type': 'textStyle/italic', 'hash': '{"type":"textStyle/italic"}' }]
|
|
],
|
|
'rollback keeps model tree up to date with removals'
|
|
);
|
|
|
|
var paragraphBreak = documentModel.prepareInsertion(
|
|
2, [{ 'type': '/paragraph' }, { 'type': 'paragraph' }]
|
|
);
|
|
|
|
// Test 13
|
|
ve.dm.TransactionProcessor.commit( documentModel, paragraphBreak );
|
|
deepEqual(
|
|
documentModel.getData( new ve.Range( 0, 7 ) ),
|
|
[
|
|
{ 'type': 'paragraph' },
|
|
'a',
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': 'paragraph' },
|
|
['b', { 'type': 'textStyle/bold', 'hash': '{"type":"textStyle/bold"}' }],
|
|
['c', { 'type': 'textStyle/italic', 'hash': '{"type":"textStyle/italic"}' }],
|
|
{ 'type': '/paragraph' }
|
|
],
|
|
'commit applies an insertion transaction that splits the paragraph'
|
|
);
|
|
|
|
// Test 14
|
|
deepEqual(
|
|
documentModel.getChildren()[0].getContentData(),
|
|
['a'],
|
|
'commit keeps model tree up to date with paragraph split (paragraph 1)'
|
|
);
|
|
|
|
// Test 15
|
|
deepEqual(
|
|
documentModel.getChildren()[1].getContentData(),
|
|
[
|
|
['b', { 'type': 'textStyle/bold', 'hash': '{"type":"textStyle/bold"}' }],
|
|
['c', { 'type': 'textStyle/italic', 'hash': '{"type":"textStyle/italic"}' }]
|
|
],
|
|
'commit keeps model tree up to date with paragraph split (paragraph 2)'
|
|
);
|
|
|
|
// Test 16
|
|
ve.dm.TransactionProcessor.rollback( documentModel, paragraphBreak );
|
|
deepEqual(
|
|
documentModel.getData( new ve.Range( 0, 5 ) ),
|
|
[
|
|
{ 'type': 'paragraph' },
|
|
'a',
|
|
['b', { 'type': 'textStyle/bold', 'hash': '{"type":"textStyle/bold"}' }],
|
|
['c', { 'type': 'textStyle/italic', 'hash': '{"type":"textStyle/italic"}' }],
|
|
{ 'type': '/paragraph' }
|
|
],
|
|
'rollback reverses the effect of a paragraph split on the content'
|
|
);
|
|
|
|
// Test 17
|
|
deepEqual(
|
|
documentModel.getChildren()[0].getContentData(),
|
|
[
|
|
'a',
|
|
['b', { 'type': 'textStyle/bold', 'hash': '{"type":"textStyle/bold"}' }],
|
|
['c', { 'type': 'textStyle/italic', 'hash': '{"type":"textStyle/italic"}' }]
|
|
],
|
|
'rollback keeps model tree up to date with paragraph split (paragraphs are merged back)'
|
|
);
|
|
|
|
// Test 18
|
|
deepEqual(
|
|
documentModel.getChildren()[1].getElementType(),
|
|
'table',
|
|
'rollback keeps model tree up to date with paragraph split (table follows the paragraph)'
|
|
);
|
|
|
|
var listItemMerge = documentModel.prepareRemoval( new ve.Range( 14, 19 ) );
|
|
|
|
// Test 19
|
|
ve.dm.TransactionProcessor.commit( documentModel, listItemMerge );
|
|
deepEqual(
|
|
documentModel.getData( new ve.Range( 12, 22 ) ),
|
|
[
|
|
{ 'type': 'listItem', 'attributes': { 'styles': ['bullet'] } },
|
|
{ 'type': 'paragraph' },
|
|
'f',
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': '/listItem' },
|
|
{ 'type': 'listItem', 'attributes': { 'styles': ['number'] } },
|
|
{ 'type': 'paragraph' },
|
|
'g',
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': '/listItem' }
|
|
],
|
|
'removal merges two list items with paragraphs'
|
|
);
|
|
|
|
// Test 20
|
|
deepEqual( documentModel.children[1].children[0].children[0].children[1].children.length, 2,
|
|
'removal keeps model tree up to date with list item merge (number of children)'
|
|
);
|
|
|
|
// Test 21
|
|
deepEqual(
|
|
documentModel.children[1].children[0].children[0].children[1].children[0].children[0].getContentData(),
|
|
[ 'f' ],
|
|
'removal keeps model tree up to date with list item merge (first list item)'
|
|
);
|
|
|
|
// Test 22
|
|
deepEqual(
|
|
documentModel.children[1].children[0].children[0].children[1].children[1].children[0].getContentData(),
|
|
[ 'g' ],
|
|
'removal keeps model tree up to date with list item merge (second list item)'
|
|
);
|
|
|
|
// Test 23
|
|
deepEqual(
|
|
documentModel.children[2].getContentData(),
|
|
[ 'h' ],
|
|
'rollback keeps model tree up to date with list item split (final paragraph)'
|
|
);
|
|
|
|
// Test 24
|
|
ve.dm.TransactionProcessor.rollback( documentModel, listItemMerge );
|
|
deepEqual(
|
|
documentModel.getData( new ve.Range( 12, 27 ) ),
|
|
[
|
|
{ 'type': 'listItem', 'attributes': { 'styles': ['bullet'] } },
|
|
{ 'type': 'paragraph' },
|
|
'e',
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': '/listItem' },
|
|
{ 'type': 'listItem', 'attributes': { 'styles': ['bullet', 'bullet'] } },
|
|
{ 'type': 'paragraph' },
|
|
'f',
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': '/listItem' },
|
|
{ 'type': 'listItem', 'attributes': { 'styles': ['number'] } },
|
|
{ 'type': 'paragraph' },
|
|
'g',
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': '/listItem' }
|
|
],
|
|
'rollback reverses list item merge (splits the list items)'
|
|
);
|
|
|
|
// Test 25
|
|
deepEqual( documentModel.children[1].children[0].children[0].children[1].children.length, 3,
|
|
'rollback keeps model tree up to date with list item split (number of children)'
|
|
);
|
|
|
|
// Test 26
|
|
deepEqual(
|
|
documentModel.children[1].children[0].children[0].children[1].children[0].children[0].getContentData(),
|
|
[ 'e' ],
|
|
'rollback keeps model tree up to date with list item split (first list item)'
|
|
);
|
|
|
|
// Test 27
|
|
deepEqual(
|
|
documentModel.children[1].children[0].children[0].children[1].children[1].children[0].getContentData(),
|
|
[ 'f' ],
|
|
'rollback keeps model tree up to date with list item split (second list item)'
|
|
);
|
|
|
|
// Test 28
|
|
deepEqual(
|
|
documentModel.children[1].children[0].children[0].children[1].children[2].children[0].getContentData(),
|
|
[ 'g' ],
|
|
'rollback keeps model tree up to date with list item split (third list item)'
|
|
);
|
|
|
|
// Test 29
|
|
deepEqual(
|
|
documentModel.children[2].getContentData(),
|
|
[ 'h' ],
|
|
'rollback keeps model tree up to date with list item split (final paragraph)'
|
|
);
|
|
|
|
var listSplit = documentModel.prepareInsertion( 17, [{ 'type': '/list' }, { 'type': 'list' }] );
|
|
|
|
// Test 30
|
|
ve.dm.TransactionProcessor.commit( documentModel, listSplit );
|
|
deepEqual(
|
|
documentModel.getData( new ve.Range( 15, 21 ) ),
|
|
[
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': '/listItem' },
|
|
{ 'type': '/list' },
|
|
{ 'type': 'list' },
|
|
{ 'type': 'listItem', 'attributes': { 'styles': ['bullet', 'bullet'] } },
|
|
{ 'type': 'paragraph' }
|
|
],
|
|
'commit splits list into two lists'
|
|
);
|
|
|
|
// Test 31
|
|
ve.dm.TransactionProcessor.rollback( documentModel, listSplit );
|
|
deepEqual(
|
|
documentModel.getData( new ve.Range( 15, 19 ) ),
|
|
[
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': '/listItem' },
|
|
{ 'type': 'listItem', 'attributes': { 'styles': ['bullet', 'bullet'] } },
|
|
{ 'type': 'paragraph' }
|
|
],
|
|
'rollback reverses list split'
|
|
);
|
|
|
|
|
|
var contentReplacement = documentModel.prepareContentReplacement( new ve.Range( 32, 33 ), [ 'i', 'j', 'k' ] );
|
|
|
|
// Test 32
|
|
ve.dm.TransactionProcessor.commit( documentModel, contentReplacement );
|
|
deepEqual(
|
|
documentModel.getData( new ve.Range( 31, 36 ) ),
|
|
[
|
|
{ 'type': 'paragraph' },
|
|
'i', 'j', 'k',
|
|
{ 'type': '/paragraph' }
|
|
],
|
|
'replacement replaces content'
|
|
);
|
|
|
|
// Test 33
|
|
ve.dm.TransactionProcessor.rollback( documentModel, contentReplacement );
|
|
deepEqual(
|
|
documentModel.getData( new ve.Range( 31, 34 ) ),
|
|
[
|
|
{ 'type': 'paragraph' },
|
|
'h',
|
|
{ 'type': '/paragraph' }
|
|
],
|
|
'rollback restores content'
|
|
);
|
|
|
|
var paragraphToHeading = documentModel.prepareWrap( new ve.Range( 1, 4 ), [ { 'type': 'paragraph' } ], [ { 'type': 'heading', 'level': 2 } ], [], [] );
|
|
|
|
// Test 34
|
|
ve.dm.TransactionProcessor.commit( documentModel, paragraphToHeading );
|
|
deepEqual(
|
|
documentModel.getData( new ve.Range( 0, 5 ) ),
|
|
[
|
|
{ 'type': 'heading', 'level': 2 },
|
|
'a',
|
|
['b', { 'type': 'textStyle/bold', 'hash': '{"type":"textStyle/bold"}' }],
|
|
['c', { 'type': 'textStyle/italic', 'hash': '{"type":"textStyle/italic"}' }],
|
|
{ 'type': '/heading' }
|
|
],
|
|
'changing paragraph to heading'
|
|
);
|
|
|
|
// Test 35
|
|
ve.dm.TransactionProcessor.rollback( documentModel, paragraphToHeading );
|
|
deepEqual(
|
|
documentModel.getData( new ve.Range( 0, 5 ) ),
|
|
[
|
|
{ 'type': 'paragraph' },
|
|
'a',
|
|
['b', { 'type': 'textStyle/bold', 'hash': '{"type":"textStyle/bold"}' }],
|
|
['c', { 'type': 'textStyle/italic', 'hash': '{"type":"textStyle/italic"}' }],
|
|
{ 'type': '/paragraph' }
|
|
],
|
|
'rollback puts paragraph back'
|
|
);
|
|
|
|
var unwrapList = documentModel.prepareWrap( new ve.Range( 12, 27 ), [ { 'type': 'list' } ], [] , [ { 'type': 'listItem' } ], [] );
|
|
|
|
// Test 36
|
|
ve.dm.TransactionProcessor.commit( documentModel, unwrapList );
|
|
deepEqual(
|
|
documentModel.getData( new ve.Range( 7, 21 ) ),
|
|
[
|
|
{ 'type': 'tableCell' },
|
|
{ 'type': 'paragraph' },
|
|
'd',
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': 'paragraph' },
|
|
'e',
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': 'paragraph' },
|
|
'f',
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': 'paragraph' },
|
|
'g',
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': '/tableCell' }
|
|
],
|
|
'unwrapping the list produces a cell with four adjacent paragraphs'
|
|
);
|
|
|
|
// Test 37
|
|
ve.dm.TransactionProcessor.rollback( documentModel, unwrapList );
|
|
deepEqual(
|
|
documentModel.getData( new ve.Range( 7, 29 ) ),
|
|
[
|
|
{ 'type': 'tableCell' },
|
|
{ 'type': 'paragraph' },
|
|
'd',
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': 'list' },
|
|
{ 'type': 'listItem', 'attributes': { 'styles': ['bullet'] } },
|
|
{ 'type': 'paragraph' },
|
|
'e',
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': '/listItem' },
|
|
{ 'type': 'listItem', 'attributes': { 'styles': ['bullet', 'bullet'] } },
|
|
{ 'type': 'paragraph' },
|
|
'f',
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': '/listItem' },
|
|
{ 'type': 'listItem', 'attributes': { 'styles': ['number'] } },
|
|
{ 'type': 'paragraph' },
|
|
'g',
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': '/listItem' },
|
|
{ 'type': '/list' },
|
|
{ 'type': '/tableCell' }
|
|
],
|
|
'rollback puts the list back'
|
|
);
|
|
|
|
var replaceTable = documentModel.prepareWrap( new ve.Range( 8, 28 ), [ { 'type': 'table' }, { 'type': 'tableRow' }, { 'type': 'tableCell' } ],
|
|
[ { 'type': 'list' }, { 'type': 'listItem' } ], [], [] );
|
|
|
|
// Test 38
|
|
ve.dm.TransactionProcessor.commit( documentModel, replaceTable );
|
|
deepEqual(
|
|
documentModel.getData( new ve.Range( 5, 30 ) ),
|
|
[
|
|
{ 'type': 'list' },
|
|
{ 'type': 'listItem' },
|
|
{ 'type': 'paragraph' },
|
|
'd',
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': 'list' },
|
|
{ 'type': 'listItem', 'attributes': { 'styles': ['bullet'] } },
|
|
{ 'type': 'paragraph' },
|
|
'e',
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': '/listItem' },
|
|
{ 'type': 'listItem', 'attributes': { 'styles': ['bullet', 'bullet'] } },
|
|
{ 'type': 'paragraph' },
|
|
'f',
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': '/listItem' },
|
|
{ 'type': 'listItem', 'attributes': { 'styles': ['number'] } },
|
|
{ 'type': 'paragraph' },
|
|
'g',
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': '/listItem' },
|
|
{ 'type': '/list' },
|
|
{ 'type': '/listItem' },
|
|
{ 'type': '/list' },
|
|
{ 'type': 'paragraph' }
|
|
],
|
|
'replacing a table with the list reverses the order of the closing tags correctly'
|
|
);
|
|
|
|
// Test 39
|
|
ve.dm.TransactionProcessor.rollback( documentModel, replaceTable );
|
|
deepEqual(
|
|
documentModel.getData( new ve.Range( 5, 32 ) ),
|
|
[
|
|
{ 'type': 'table' },
|
|
{ 'type': 'tableRow' },
|
|
{ 'type': 'tableCell' },
|
|
{ 'type': 'paragraph' },
|
|
'd',
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': 'list' },
|
|
{ 'type': 'listItem', 'attributes': { 'styles': ['bullet'] } },
|
|
{ 'type': 'paragraph' },
|
|
'e',
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': '/listItem' },
|
|
{ 'type': 'listItem', 'attributes': { 'styles': ['bullet', 'bullet'] } },
|
|
{ 'type': 'paragraph' },
|
|
'f',
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': '/listItem' },
|
|
{ 'type': 'listItem', 'attributes': { 'styles': ['number'] } },
|
|
{ 'type': 'paragraph' },
|
|
'g',
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': '/listItem' },
|
|
{ 'type': '/list' },
|
|
{ 'type': '/tableCell' },
|
|
{ 'type': '/tableRow' },
|
|
{ 'type': '/table' },
|
|
{ 'type': 'paragraph' }
|
|
],
|
|
'rollback puts the table back'
|
|
);
|
|
|
|
} );
|