Add basic support for about groups

About groups are HTML structures like the following:
<div about="#mwt1">....</div>
<span about="#mwt1">...</span>
<div about="#mwt1">...</div>
When about groups are alienated, they are now merged into one alien
node, rather than producing a separate alien node for each sibling.

This is very basic about group handling, because it only works for
groups of directly adjacent siblings (text nodes are permitted in
between, but nothing else) assumes all about groups are aliens (which
is currently true).

* Before processing an element in the DOM->data converter, perform about
  grouping on its children. This temporarily wraps about groups in
  <div data-ve-aboutgroup="value of about attribute">
* Extended createAlien() to handle single nodes as well as wrappers
  holding multiple nodes.
* In the data->DOM converter, temporarily wrap multi-node aliens in
  <div data-ve-multi-child-alien-wrapper="true"> . This makes the rest
  of the algorithm easier.

Change-Id: I2df5f62bc222b570fc11a89fe43d353f8363ead8
This commit is contained in:
Catrope 2012-11-07 18:03:05 -08:00
parent f10ca89888
commit d4ea93b872
3 changed files with 162 additions and 12 deletions

View file

@ -206,13 +206,18 @@ ve.dm.Converter.prototype.getDomElementFromDataAnnotation = function ( dataAnnot
* @returns {Array} Linear model data
*/
ve.dm.Converter.prototype.getDataFromDom = function ( domElement, annotations, dataElement, path, alreadyWrapped ) {
function createAlien( domElement, isInline ) {
var type = isInline ? 'alienInline' : 'alienBlock';
function createAlien( domElement, isInline, isWrapper ) {
var type = isInline ? 'alienInline' : 'alienBlock', html;
if ( isWrapper ) {
html = $( domElement ).html();
} else {
html = $( '<div>' ).append( $( domElement ).clone() ).html();
}
return [
{
'type': type,
'attributes': {
'html': $( '<div>' ).append( $( domElement ).clone() ).html()
'html': html
}
},
{ 'type': '/' + type }
@ -243,6 +248,64 @@ ve.dm.Converter.prototype.getDataFromDom = function ( domElement, annotations, d
}
}
/**
* Helper function to group adjacent child elements with the same about attribute together.
* If there are multiple adjacent child nodes with the same about attribute, they are
* wrapped in a <div> with the data-ve-aboutgroup attribute set.
*
* This function does not wrap single-element about groups, and does not descend into the
* child elements.
*
* @param element {HTMLElement} Element to process
*/
function doAboutGrouping( element ) {
var child = element.firstChild, textNodes = [],
prevChild, aboutGroup, aboutWrapper, childAbout, nextChild, i;
while ( child ) {
nextChild = child.nextSibling;
if ( !child.getAttribute ) {
// Text nodes don't have a getAttribute() method. Thanks HTML DOM,
// that's really helpful ^^
textNodes.push( child );
child = nextChild;
continue;
}
childAbout = child.getAttribute( 'about' );
if ( childAbout && !aboutGroup ) {
// Start of a new about group
aboutGroup = childAbout;
} else if ( childAbout && childAbout === aboutGroup ) {
// Continuation of the current about group
if ( !aboutWrapper ) {
// This is the second child in this group, so the
// previous child is the first child in this group.
// Wrap the previous child
aboutWrapper = document.createElement( 'div' );
aboutWrapper.setAttribute( 'data-ve-aboutgroup', aboutGroup );
element.insertBefore( aboutWrapper, prevChild );
aboutWrapper.appendChild( prevChild );
}
// Append any outstanding text nodes to the wrapper
for ( i = 0; i < textNodes.length; i++ ) {
aboutWrapper.appendChild( textNodes[i] );
}
// Append this child to the wrapper
aboutWrapper.appendChild( child );
} else if ( aboutGroup ) {
// This child isn't in the current about group
aboutGroup = undefined;
aboutWrapper = undefined;
if ( childAbout ) {
// Start of a new about group
aboutGroup = childAbout;
}
}
prevChild = child;
child = nextChild;
textNodes = [];
}
}
// Fallback to defaults
annotations = annotations || [];
path = path || ['document'];
@ -259,11 +322,23 @@ ve.dm.Converter.prototype.getDataFromDom = function ( domElement, annotations, d
if ( dataElement ) {
data.push( dataElement );
}
// Do about grouping
// FIXME this assumes every about group is an alien
doAboutGrouping( domElement );
// Add contents
for ( i = 0; i < domElement.childNodes.length; i++ ) {
childDomElement = domElement.childNodes[i];
switch ( childDomElement.nodeType ) {
case Node.ELEMENT_NODE:
// Alienate about groups
if ( childDomElement.hasAttribute( 'data-ve-aboutgroup' ) ) {
alien = createAlien( childDomElement, branchIsContent, true );
data = data.concat( alien );
processNextWhitespace( alien[0] );
prevElement = alien[0];
break;
}
// HACK handle <meta>/<link> separately because of the
// metaInline/metaBlock distinction
if (
@ -795,13 +870,18 @@ ve.dm.Converter.prototype.getDomFromData = function ( data ) {
// Create nodes from source
wrapper = document.createElement( 'div' );
wrapper.innerHTML = dataElement.attributes.html;
// Add element - adds first child element, any other
// children are ignored but shouldn't exist
//parentDomElement = domElement;
childDomElement = wrapper.firstChild;
//parentDomElement.appendChild( domElement );
// Make sure the alien closing is skipped
//parentDomElement = domElement; domElement = wrapper; //i++;
if ( wrapper.childNodes.length > 1 ) {
// Wrap the HTML in a single element, this makes
// it much easier to deal with. It'll be unwrapped
// at the end of this function.
childDomElement = document.createElement( 'div' );
childDomElement.setAttribute( 'data-ve-multi-child-alien-wrapper', 'true' );
while ( wrapper.firstChild ) {
childDomElement.appendChild( wrapper.firstChild );
}
} else {
childDomElement = wrapper.firstChild;
}
} else {
// Create node from data
childDomElement = this.getDomElementFromDataElement( dataElement );
@ -870,6 +950,11 @@ ve.dm.Converter.prototype.getDomFromData = function ( data ) {
}
delete container.lastOuterPost;
}
// Unwrap multi-child alien wrappers
$( container ).find( '[data-ve-multi-child-alien-wrapper]' ) .each( function() {
$( this ).replaceWith( $( this ).contents() );
} );
return container;
};

View file

@ -35,7 +35,7 @@ QUnit.test( 'getDomElementFromDataElement', 20, function ( assert ) {
}
} );
QUnit.test( 'getDataFromDom', 31, function ( assert ) {
QUnit.test( 'getDataFromDom', 33, function ( assert ) {
var msg,
cases = ve.dm.example.domToDataCases;
@ -51,7 +51,7 @@ QUnit.test( 'getDataFromDom', 31, function ( assert ) {
}
} );
QUnit.test( 'getDomFromData', 33, function ( assert ) {
QUnit.test( 'getDomFromData', 35, function ( assert ) {
var msg,
cases = ve.dm.example.domToDataCases;

View file

@ -1507,5 +1507,70 @@ ve.dm.example.domToDataCases = {
'normalizedHtml': '<p data-ve-changed="{&quot;content&quot;:1}">' +
'Foo<img data-ve-changed="{&quot;attributes&quot;:2}" />' +
'</p><p data-ve-changed="{&quot;created&quot;:1}">Bar</p>'
},
'about grouping': {
'html': '<div typeof="mw:Placeholder" about="#mwt1">Foo</div>' +
'<figure typeof="mw:Placeholder" about="#mwt1">Bar</figure>' +
'<figure typeof="mw:Placeholder" about="#mwt2">Baz</figure>' +
'<span typeof="mw:Placeholder" about="#mwt2">Quux</span>' +
'<p>Whee</p><span typeof="mw:Placeholder" about="#mwt2">Yay</span>' +
'<div typeof="mw:Placeholder" about="#mwt2">Blah</div>' +
'<span typeof="mw:Placeholder" about="#mwt3">Meh</span>',
'data': [
{
'type': 'alienBlock',
'attributes': {
'html': '<div typeof="mw:Placeholder" about="#mwt1">Foo</div>' +
'<figure typeof="mw:Placeholder" about="#mwt1">Bar</figure>'
}
},
{ 'type': '/alienBlock' },
{
'type': 'alienBlock',
'attributes': {
'html': '<figure typeof="mw:Placeholder" about="#mwt2">Baz</figure>' +
'<span typeof="mw:Placeholder" about="#mwt2">Quux</span>'
}
},
{ 'type': '/alienBlock' },
{ 'type': 'paragraph' },
'W',
'h',
'e',
'e',
{ 'type': '/paragraph' },
{
'type': 'alienBlock',
'attributes': {
'html': '<span typeof="mw:Placeholder" about="#mwt2">Yay</span>' +
'<div typeof="mw:Placeholder" about="#mwt2">Blah</div>'
}
},
{ 'type': '/alienBlock' },
{
'type': 'alienBlock',
'attributes': {
'html': '<span typeof="mw:Placeholder" about="#mwt3">Meh</span>'
}
},
{ 'type': '/alienBlock' }
]
},
'whitespace preservation with an about group': {
'html': ' <div typeof="mw:Placeholder" about="#mwt1">\tFoo\t\t</div>\t\t\t' +
'<div typeof="mw:Placeholder" about="#mwt1"> Bar </div> ',
'data': [
{
'type': 'alienBlock',
'attributes': {
'html': '<div typeof="mw:Placeholder" about="#mwt1">\tFoo\t\t</div>\t\t\t' +
'<div typeof="mw:Placeholder" about="#mwt1"> Bar </div>'
},
'internal': {
'whitespace': [ ' ', undefined, undefined, ' ' ]
}
},
{ 'type': '/alienBlock' }
]
}
};