Store inner whitespace of the body and compare it on conversion

Calculate and store the two inner whitespace values of the body in the
dm.Document. When converting back, make sure the first/last nodes
pre/post outer whitespace matches the inner left/right whitespace
of the body.

Bug: 54964
Change-Id: I45f1ffd63669f25a6cae878400bfe21719ed58ee
This commit is contained in:
Ed Sanders 2013-10-31 15:49:49 +01:00
parent 91f6b1d86c
commit 3838498e8b
5 changed files with 89 additions and 31 deletions

View file

@ -736,11 +736,11 @@ ve.init.mw.ViewPageTarget.prototype.onSaveDialogReview = function () {
if ( this.pageExists ) {
// Has no callback, handled via target.onShowChanges
this.showChanges(
ve.dm.converter.getDomFromData( doc.getFullData(), doc.getStore(), doc.getInternalList() )
ve.dm.converter.getDomFromData( doc.getFullData(), doc.getStore(), doc.getInternalList(), doc.getInnerWhitespace() )
);
} else {
this.serialize(
ve.dm.converter.getDomFromData( doc.getFullData(), doc.getStore(), doc.getInternalList() ),
ve.dm.converter.getDomFromData( doc.getFullData(), doc.getStore(), doc.getInternalList(), doc.getInnerWhitespace() ),
ve.bind( this.onSerialize, this )
);
}
@ -786,7 +786,7 @@ ve.init.mw.ViewPageTarget.prototype.saveDocument = function () {
this.saveDialog.saveButton.setDisabled( true );
this.saveDialog.$loadingIcon.show();
this.save(
ve.dm.converter.getDomFromData( doc.getFullData(), doc.getStore(), doc.getInternalList() ),
ve.dm.converter.getDomFromData( doc.getFullData(), doc.getStore(), doc.getInternalList(), doc.getInnerWhitespace() ),
saveOptions
);
}
@ -856,10 +856,11 @@ ve.init.mw.ViewPageTarget.prototype.setUpSurface = function ( doc, callback ) {
// Build linmod
var store = new ve.dm.IndexValueStore(),
internalList = new ve.dm.InternalList(),
data = ve.dm.converter.getDataFromDom( doc, store, internalList );
innerWhitespace = new Array( 2 ),
data = ve.dm.converter.getDataFromDom( doc, store, internalList, innerWhitespace );
setTimeout( function () {
// Build DM tree
var dmDoc = new ve.dm.Document( data, doc, undefined, internalList );
var dmDoc = new ve.dm.Document( data, doc, undefined, internalList, innerWhitespace );
setTimeout( function () {
// Create ui.Surface (also creates ce.Surface and dm.Surface and builds CE tree)
target.surface = new ve.ui.Surface( dmDoc, target.surfaceOptions );
@ -929,8 +930,8 @@ ve.init.mw.ViewPageTarget.prototype.startSanityCheck = function () {
// <body> were ignored in the conversion. So compare each child separately.
var i,
len = oldDom.body.childNodes.length,
newDoc = new ve.dm.Document( data, oldDom, undefined, doc.getInternalList() ),
newDom = ve.dm.converter.getDomFromData( newDoc.getFullData(), newDoc.getStore(), newDoc.getInternalList() );
newDoc = new ve.dm.Document( data, oldDom, undefined, doc.getInternalList(), doc.getInnerWhitespace() ),
newDom = ve.dm.converter.getDomFromData( newDoc.getFullData(), newDoc.getStore(), newDoc.getInternalList(), newDoc.getInnerWhitespace() );
// Explicitly unlink our full copy of the original version of the document data
data = undefined;

View file

@ -385,9 +385,10 @@ ve.dm.Converter.prototype.getDomElementFromDataAnnotation = function ( dataAnnot
* @param {HTMLDocument} doc HTML document to convert
* @param {ve.dm.IndexValueStore} store Index-value store
* @param {ve.dm.InternalList} internalList Internal list
* @param {Array} innerWhitespace Inner whitespace
* @returns {ve.dm.FlatLinearData} Linear model data
*/
ve.dm.Converter.prototype.getDataFromDom = function ( doc, store, internalList ) {
ve.dm.Converter.prototype.getDataFromDom = function ( doc, store, internalList, innerWhitespace ) {
var linearData, refData;
// Set up the converter state
@ -404,6 +405,8 @@ ve.dm.Converter.prototype.getDataFromDom = function ( doc, store, internalList )
refData = this.internalList.convertToData( this, doc );
linearData.batchSplice( linearData.getLength(), 0, refData );
this.setInnerWhitespace( innerWhitespace, linearData );
// Clear the state
this.doc = null;
this.store = null;
@ -976,6 +979,39 @@ ve.dm.Converter.prototype.getDataFromDomRecursion = function ( domElement, wrapp
return data;
};
/**
* Set inner whitespace from linear data
*
* @param {Array} innerWhitespace Inner whitespace
* @param {ve.dm.FlatLinearData} data Linear model data
*/
ve.dm.Converter.prototype.setInnerWhitespace = function ( innerWhitespace, data ) {
var whitespace,
stack = 0,
last = data.getLength() - 1;
if ( data.isOpenElementData( 0 ) ) {
whitespace = ve.getProp( data.getData( 0 ), 'internal', 'whitespace' );
innerWhitespace[0] = whitespace ? whitespace[0] : undefined;
}
if ( data.isCloseElementData( last ) ) {
// Find matching opening tag of the last close tag
stack++;
while ( --last ) {
if ( data.isCloseElementData( last ) ) {
stack++;
} else if ( data.isOpenElementData( last ) ) {
stack--;
if ( stack === 0 && data.getType( last ) !== 'internalList' ) {
break;
}
}
}
whitespace = ve.getProp( data.getData( last ), 'internal', 'whitespace' );
innerWhitespace[1] = whitespace ? whitespace[3] : undefined;
}
};
/**
* Check if all the domElements provided are metadata or whitespace.
*
@ -1031,9 +1067,10 @@ ve.dm.Converter.prototype.isDomAllMetaOrWhitespace = function ( domElements, exc
* @param {Array} documentData Linear model data
* @param {ve.dm.IndexValueStore} store Index-value store
* @param {ve.dm.InternalList} internalList Internal list
* @param {Array} innerWhitespace Inner whitespace
* @returns {HTMLDocument} Document containing the resulting HTML
*/
ve.dm.Converter.prototype.getDomFromData = function ( documentData, store, internalList ) {
ve.dm.Converter.prototype.getDomFromData = function ( documentData, store, internalList, innerWhitespace ) {
var doc = ve.createDocumentFromHtml( '' );
// Set up the converter state
@ -1041,7 +1078,7 @@ ve.dm.Converter.prototype.getDomFromData = function ( documentData, store, inter
this.store = store;
this.internalList = internalList;
this.getDomSubtreeFromData( documentData, doc.body );
this.getDomSubtreeFromData( documentData, doc.body, innerWhitespace );
// Clear the state
this.documentData = null;
@ -1056,9 +1093,10 @@ ve.dm.Converter.prototype.getDomFromData = function ( documentData, store, inter
*
* @param {Array} data Linear model data
* @param {HTMLElement} container DOM element to add the generated elements to. Should be empty.
* @param {Array} [innerWhitespace] Inner whitespace if the container is the body
* @throws Unbalanced data: looking for closing /type
*/
ve.dm.Converter.prototype.getDomSubtreeFromData = function ( data, container ) {
ve.dm.Converter.prototype.getDomSubtreeFromData = function ( data, container, innerWhitespace ) {
var text, i, j, isStart, annotations, dataElement, dataElementOrSlice,
childDomElements, pre, ours, theirs, parentDomElement, lastChild, isContentNode, sibling,
previousSiblings, doUnwrap, textNode, type, annotatedDomElementStack, annotatedDomElements,
@ -1440,9 +1478,8 @@ ve.dm.Converter.prototype.getDomSubtreeFromData = function ( data, container ) {
// Get previous sibling's outerPost
theirs = parentDomElement.lastOuterPost;
} else if ( parentDomElement === container ) {
// outerPre of the very first node in the document, this one
// has no duplicate
theirs = ours;
// outerPre of the very first node in the document, check against body innerWhitespace
theirs = innerWhitespace ? innerWhitespace[0] : ours;
} else {
// First child, get parent's innerPre
if (
@ -1473,8 +1510,11 @@ ve.dm.Converter.prototype.getDomSubtreeFromData = function ( data, container ) {
}
}
}
// Process the outerPost whitespace of the very last node
if ( container.lastOuterPost !== undefined ) {
// Check outerPost whitespace of the very last node against body innerWhitespace
if (
container.lastOuterPost !== undefined &&
( !innerWhitespace || container.lastOuterPost === innerWhitespace[1] )
) {
if ( container.lastChild && container.lastChild.nodeType === Node.TEXT_NODE ) {
// Last child is a TextNode, append to it
container.lastChild.appendData( container.lastOuterPost );

View file

@ -23,8 +23,9 @@
* ignored.
* @param {ve.dm.Document} [parentDocument] Document to use as root for created nodes
* @param {ve.dm.InternalList} [internalList] Internal list to clone; passed when creating a document slice
* @param {Array} [innerWhitespace] Inner whitespace to clone; passed when creating a document slice
*/
ve.dm.Document = function VeDmDocument( data, htmlDocument, parentDocument, internalList ) {
ve.dm.Document = function VeDmDocument( data, htmlDocument, parentDocument, internalList, innerWhitespace ) {
// Parent constructor
ve.Document.call( this, new ve.dm.DocumentNode() );
@ -37,6 +38,7 @@ ve.dm.Document = function VeDmDocument( data, htmlDocument, parentDocument, inte
this.documentNode.setRoot( root );
this.documentNode.setDocument( doc );
this.internalList = internalList ? internalList.clone( this ) : new ve.dm.InternalList( this );
this.innerWhitespace = innerWhitespace ? ve.copy( innerWhitespace ) : new Array( 2 );
// Properties
this.parentDocument = parentDocument;
@ -51,7 +53,7 @@ ve.dm.Document = function VeDmDocument( data, htmlDocument, parentDocument, inte
fullData = data;
} else if ( !ve.isArray( data ) && typeof data === 'object' ) {
// HTMLDocument
fullData = ve.dm.converter.getDataFromDom( data, new ve.dm.IndexValueStore(), this.getInternalList() );
fullData = ve.dm.converter.getDataFromDom( data, new ve.dm.IndexValueStore(), this.getInternalList(), this.getInnerWhitespace() );
htmlDocument = data;
} else {
// Raw linear model data
@ -352,6 +354,14 @@ ve.dm.Document.prototype.getInternalList = function () {
return this.internalList;
};
/**
* Get the document's inner whitespace
* @returns {Array} The document's inner whitespace
*/
ve.dm.Document.prototype.getInnerWhitespace = function () {
return this.innerWhitespace;
};
/**
* Clone a sub-document from a range in this document. The new document's store and internal list will be
* clones of the ones in this document.

View file

@ -1787,7 +1787,8 @@ ve.dm.example.domToDataCases = {
{ 'type': '/list' },
{ 'type': 'internalList' },
{ 'type': '/internalList' }
]
],
'innerWhitespace': [ '\n', '\t\n\t\n' ]
},
'outer whitespace preservation in a list with bare text and a sublist': {
'body': '<ul>\n<li>\n\nBa re\n\n\n<ul>\n\n\n\n<li> <p> P </p> </li>\t</ul>\t\t</li>\t\t\t</ul>',
@ -1855,7 +1856,8 @@ ve.dm.example.domToDataCases = {
{ 'type': '/paragraph' },
{ 'type': 'internalList' },
{ 'type': '/internalList' }
]
],
'innerWhitespace': [ undefined, ' ' ]
},
'whitespace preservation with non-edge content whitespace with nested annotations': {
'body': '<p> A B <b> C\t<i>\t\tD\t\t\t</i>\t\t\t\tE\n</b>\n\nF\n\n\n</p>',
@ -2026,7 +2028,8 @@ ve.dm.example.domToDataCases = {
{ 'type': '/alienBlock' },
{ 'type': 'internalList' },
{ 'type': '/internalList' }
]
],
'innerWhitespace': [ ' ', ' \n ' ]
},
'whitespace preservation not triggered inside <pre>': {
'body': '\n<pre>\n\n\nFoo\n\n\nBar\n\n\n\n</pre>\n\n\n\n\n',
@ -2050,7 +2053,8 @@ ve.dm.example.domToDataCases = {
{ 'type': '/preformatted' },
{ 'type': 'internalList' },
{ 'type': '/internalList' }
]
],
'innerWhitespace': [ '\n', '\n\n\n\n\n' ]
},
'whitespace preservation in table cell starting with text and ending with annotation': {
'body': '<table><tbody><tr><td>Foo <b>Bar</b></td></tr></tbody></table>',
@ -2343,7 +2347,8 @@ ve.dm.example.domToDataCases = {
{ 'type': 'internalList' },
{ 'type': '/internalList' }
],
'normalizedBody': ' <ul><li><p>\tA\n</p> <p>B</p></li></ul> '
'innerWhitespace': [ '\t', '\n' ],
'normalizedBody': '<ul><li><p>\tA\n</p> <p>B</p></li></ul>'
},
'order of nested annotations is preserved': {
'body': '<p><b><u><i>Foo</i></u></b></p>',
@ -2556,7 +2561,8 @@ ve.dm.example.domToDataCases = {
{ 'type': '/alienBlock' },
{ 'type': 'internalList' },
{ 'type': '/internalList' }
]
],
'innerWhitespace': [ ' ', ' ' ]
},
'block node inside annotation node is alienated': {
'body': '<span>\n<p>Bar</p></span>',

View file

@ -48,7 +48,7 @@ ve.test.utils.runFormatConverterTest = function ( assert, range, type, attribute
};
ve.test.utils.runGetDataFromDomTests = function( assert, cases ) {
var msg, doc, store, internalList, i, length, hash, data, html, n = 0;
var msg, doc, store, i, length, hash, data, html, n = 0;
// TODO: this is a hack to make normal heading/preformatted
// nodes the most recently registered, instead of the MW versions
@ -57,7 +57,7 @@ ve.test.utils.runGetDataFromDomTests = function( assert, cases ) {
for ( msg in cases ) {
if ( cases[msg].head !== undefined || cases[msg].body !== undefined ) {
n++;
n += 2;
if ( cases[msg].storeItems ) {
n += cases[msg].storeItems.length;
}
@ -69,14 +69,14 @@ ve.test.utils.runGetDataFromDomTests = function( assert, cases ) {
if ( cases[msg].head !== undefined || cases[msg].body !== undefined ) {
doc = new ve.dm.Document( [] );
store = doc.getStore();
internalList = doc.getInternalList();
html = '<head>' + ( cases[msg].head || '' ) + '</head><body>' + cases[msg].body + '</body>';
data = ve.dm.converter.getDataFromDom(
ve.createDocumentFromHtml( html ), store, internalList
).getData();
ve.createDocumentFromHtml( html ), store, doc.getInternalList(), doc.getInnerWhitespace()
);
ve.dm.example.preprocessAnnotations( cases[msg].data, store );
assert.deepEqualWithDomElements( data, cases[msg].data, msg );
assert.deepEqualWithDomElements( data.getData(), cases[msg].data, msg + ': data' );
assert.deepEqual( doc.getInnerWhitespace(), cases[msg].innerWhitespace || new Array( 2 ), msg + ': inner whitespace' );
// check storeItems have been added to store
if ( cases[msg].storeItems ) {
for ( i = 0, length = cases[msg].storeItems.length; i < length; i++ ) {
@ -112,10 +112,11 @@ ve.test.utils.runGetDomFromDataTests = function( assert, cases ) {
cases[msg].modify( cases[msg].data );
}
doc = new ve.dm.Document( ve.dm.example.preprocessAnnotations( cases[msg].data, store ) );
doc.innerWhitespace = cases[msg].innerWhitespace ? ve.copy( cases[msg].innerWhitespace ) : new Array( 2 );
originalData = ve.copy( doc.getFullData() );
html = '<body>' + ( cases[msg].normalizedBody || cases[msg].body ) + '</body>';
assert.equalDomElement(
ve.dm.converter.getDomFromData( doc.getFullData(), doc.getStore(), doc.getInternalList() ),
ve.dm.converter.getDomFromData( doc.getFullData(), doc.getStore(), doc.getInternalList(), doc.getInnerWhitespace() ),
ve.createDocumentFromHtml( html ),
msg
);