Merge changes I2edc46b6,Iea905f2b,Ia572fd06,Id81da87b,I98a7d3eb

* changes:
  Add a node class for mw:Nowiki
  Move getHashObject() from dm.Node up into dm.Model
  Allow annotations to render nothing
  Pass child DOM elements to annotations' toDomElements()
  Process annotations bottom-up rather than top-down in data->DOM
This commit is contained in:
jenkins-bot 2013-07-19 19:11:38 +00:00 committed by Gerrit Code Review
commit 21e47c5a33
9 changed files with 274 additions and 61 deletions

View file

@ -358,6 +358,7 @@ $wgResourceModules += array(
've-mw/dm/annotations/ve.dm.MWExternalLinkAnnotation.js',
've-mw/dm/annotations/ve.dm.MWInternalLinkAnnotation.js',
've/dm/annotations/ve.dm.TextStyleAnnotation.js',
've-mw/dm/annotations/ve.dm.MWNowikiAnnotation.js',
've/dm/metaitems/ve.dm.AlienMetaItem.js',
've-mw/dm/metaitems/ve.dm.MWAlienMetaItem.js',
@ -431,6 +432,7 @@ $wgResourceModules += array(
've-mw/ce/annotations/ve.ce.MWExternalLinkAnnotation.js',
've-mw/ce/annotations/ve.ce.MWInternalLinkAnnotation.js',
've/ce/annotations/ve.ce.TextStyleAnnotation.js',
've-mw/ce/annotations/ve.ce.MWNowikiAnnotation.js',
// ui
've/ui/ve.ui.js',

View file

@ -0,0 +1,37 @@
/*!
* VisualEditor ContentEditable MWNowikiAnnotation class.
*
* @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* ContentEditable MediaWiki nowiki annotation.
*
* @class
* @extends ve.ce.Annotation
* @constructor
* @param {ve.dm.MWNowikiAnnotation} model Model to observe
* @param {Object} [config] Config options
*/
ve.ce.MWNowikiAnnotation = function VeCeMWInternalLinkAnnotation( model, config ) {
// Parent constructor
ve.ce.Annotation.call( this, model, config );
// DOM changes
this.$.addClass( 've-ce-mwNowikiAnnotation' );
};
/* Inheritance */
ve.inheritClass( ve.ce.MWNowikiAnnotation, ve.ce.Annotation );
/* Static Properties */
ve.ce.MWNowikiAnnotation.static.name = 'mwNowiki';
ve.ce.MWNowikiAnnotation.static.tagName = 'span';
/* Registration */
ve.ce.annotationFactory.register( ve.ce.MWNowikiAnnotation );

View file

@ -0,0 +1,84 @@
/*!
* VisualEditor DataModel MWNowikiAnnotation class.
*
* @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* DataModel MediaWiki nowiki annotation
*
* Represents `<nowiki>` tags (in HTML as `<span typeof="mw:Nowiki">`) and unwraps them when they change
* so as to retrigger Parsoid's escaping mechanism.
*
* @class
* @extends ve.dm.Annotation
* @constructor
* @param {Object} element [description]
*/
ve.dm.MWNowikiAnnotation = function VeDmMwNowikiAnnotation( element ) {
// Parent constructor
ve.dm.Annotation.call( this, element );
};
/* Inheritance */
ve.inheritClass( ve.dm.MWNowikiAnnotation, ve.dm.Annotation );
/* Static Properties */
ve.dm.MWNowikiAnnotation.static.name = 'mwNowiki';
ve.dm.MWNowikiAnnotation.static.matchRdfaTypes = [ 'mw:Nowiki' ];
ve.dm.MWNowikiAnnotation.static.toDataElement = function ( domElements ) {
return {
'type': 'mwNowiki',
'attributes': {
'originalDomElements': ve.copyArray( domElements )
}
};
};
ve.dm.MWNowikiAnnotation.static.toDomElements = function ( dataElement, doc, converter, childDomElements ) {
var i, len,
originalDomElements = dataElement.attributes.originalDomElements,
originalChildren = originalDomElements && originalDomElements[0] && originalDomElements[0].childNodes,
contentsChanged = false,
domElement = document.createElement( 'span' );
// Determine whether the contents changed
if ( !originalChildren || childDomElements.length !== originalChildren.length ) {
contentsChanged = true;
} else {
for ( i = 0, len = originalChildren.length; i < len; i++ ) {
if ( !originalChildren[i].isEqualNode( childDomElements[i] ) ) {
contentsChanged = true;
break;
}
}
}
// If the contents changed, unwrap, otherwise, restore
if ( contentsChanged ) {
return [];
}
domElement.setAttribute( 'typeof', 'mw:Nowiki' );
return [ domElement ];
};
ve.dm.MWNowikiAnnotation.static.getHashObject = function ( dataElement ) {
var parentResult = ve.dm.Annotation.static.getHashObject( dataElement );
if ( parentResult.attributes.originalDomElements ) {
// If present, replace originalDomElements with a DOM summary
parentResult.attributes = ve.copyObject( parentResult.attributes );
parentResult.attributes.originalDomElements = ve.copyArray(
parentResult.attributes.originalDomElements, ve.convertDomElements
);
}
return parentResult;
};
/* Registration */
ve.dm.modelRegistry.register( ve.dm.MWNowikiAnnotation );

View file

@ -121,6 +121,32 @@ ve.dm.mwExample.MWTransclusion.mixedStoreItems = {
'value': $( ve.dm.mwExample.MWTransclusion.mixed ).toArray()
};
ve.dm.mwExample.mwNowikiAnnotation = {
'type': 'mwNowiki',
'attributes': {
'originalDomElements': $( '<span typeof="mw:Nowiki">[[Bar]]</span>' ).toArray()
},
'htmlAttributes': [ { 'values': { 'typeof': 'mw:Nowiki' } } ]
};
ve.dm.mwExample.mwNowiki = [
{ 'type': 'paragraph' },
'F', 'o', 'o',
[ '[', [ ve.dm.mwExample.mwNowikiAnnotation ] ],
[ '[', [ ve.dm.mwExample.mwNowikiAnnotation ] ],
[ 'B', [ ve.dm.mwExample.mwNowikiAnnotation ] ],
[ 'a', [ ve.dm.mwExample.mwNowikiAnnotation ] ],
[ 'r', [ ve.dm.mwExample.mwNowikiAnnotation ] ],
[ ']', [ ve.dm.mwExample.mwNowikiAnnotation ] ],
[ ']', [ ve.dm.mwExample.mwNowikiAnnotation ] ],
'B', 'a', 'z',
{ 'type': '/paragraph' },
{ 'type': 'internalList' },
{ 'type': '/internalList' }
];
ve.dm.mwExample.mwNowikiHtml = '<body><p>Foo<span typeof="mw:Nowiki">[[Bar]]</span>Baz</p></body>';
ve.dm.mwExample.withMeta = [
{
'type': 'alienMeta',
@ -1598,5 +1624,25 @@ ve.dm.mwExample.domToDataCases = {
{ 'type': 'internalList' },
{ 'type': '/internalList' }
]
},
'mw:Nowiki': {
'html': ve.dm.mwExample.mwNowikiHtml,
'data': ve.dm.mwExample.mwNowiki
},
'mw:Nowiki unwraps when text modified': {
'html': null,
'data': ve.dm.mwExample.mwNowiki,
'modify': function ( data ) {
data[7][0] = 'z';
},
'normalizedHtml': '<body><p>Foo[[Bzr]]Baz</p></body>'
},
'mw:Nowiki unwraps when annotations modified': {
'html': null,
'data': ve.dm.mwExample.mwNowiki,
'modify': function ( data ) {
data[7][1].push( ve.dm.example.bold );
},
'normalizedHtml': '<body><p>Foo[[B<b>a</b>r]]Baz</p></body>'
}
};

View file

@ -129,6 +129,7 @@
<script src="../../ve-mw/dm/annotations/ve.dm.MWExternalLinkAnnotation.js"></script>
<script src="../../ve-mw/dm/annotations/ve.dm.MWInternalLinkAnnotation.js"></script>
<script src="../../ve/dm/annotations/ve.dm.TextStyleAnnotation.js"></script>
<script src="../../ve-mw/dm/annotations/ve.dm.MWNowikiAnnotation.js"></script>
<script src="../../ve/dm/metaitems/ve.dm.AlienMetaItem.js"></script>
<script src="../../ve-mw/dm/metaitems/ve.dm.MWAlienMetaItem.js"></script>
<script src="../../ve-mw/dm/metaitems/ve.dm.MWCategoryMetaItem.js"></script>
@ -192,6 +193,7 @@
<script src="../../ve-mw/ce/annotations/ve.ce.MWExternalLinkAnnotation.js"></script>
<script src="../../ve-mw/ce/annotations/ve.ce.MWInternalLinkAnnotation.js"></script>
<script src="../../ve/ce/annotations/ve.ce.TextStyleAnnotation.js"></script>
<script src="../../ve-mw/ce/annotations/ve.ce.MWNowikiAnnotation.js"></script>
<script src="../../ve/ui/ve.ui.js"></script>
<script src="../../ve/ui/ve.ui.Surface.js"></script>
<script src="../../ve/ui/ve.ui.Context.js"></script>

View file

@ -48,6 +48,28 @@ ve.dm.Annotation.static.enableAboutGrouping = false;
*/
ve.dm.Annotation.static.applyToAppendedContent = true;
/**
* Static function to convert a linear model data element for this annotation type back to
* a DOM element.
*
* As special facilities for annotations, the annotated content that the returned element will
* wrap around is passed in as childDomElements, and this function may return an empty array to
* indicate that the annotation should produce no output. In that case, the child DOM elements will
* not be wrapped in anything and will be inserted directly into this annotation's parent.
*
* @static
* @inheritable
* @method
* @param {Object|Array} dataElement Linear model element or array of linear model data
* @param {HTMLDocument} doc HTML document for creating elements
* @param {ve.dm.Converter} converter Converter object to optionally call .getDomSubtreeFromData() on
* @param {HTMLElement[]} childDomElements Children that will be appended to the returned element
* @returns {HTMLElement[]} Array of DOM elements; only the first element is used; may be empty
*/
ve.dm.Annotation.static.toDomElements = function ( /*dataElement, doc, converter, childDomElements*/ ) {
throw new Error( 've.dm.Annotation subclass must implement toDomElements' );
};
/* Methods */
/**
@ -68,10 +90,9 @@ ve.dm.Annotation.prototype.getDomElements = function ( doc ) {
* @returns {Object} An object containing a subset of the annotation's properties
*/
ve.dm.Annotation.prototype.getComparableObject = function () {
return {
'type': this.getType(),
'attributes': this.getAttributes()
};
var hashObject = this.getHashObject();
delete hashObject.htmlAttributes;
return hashObject;
};
/**

View file

@ -65,7 +65,7 @@ ve.dm.Converter.getDataContentFromText = function ( text, annotations ) {
* @param {ve.dm.AnnotationSet} currentSet The set of annotations currently opened. Will be modified.
* @param {ve.dm.AnnotationSet} targetSet The set of annotations we want to have.
* @param {Function} open Callback called when an annotation is opened. Passed a ve.dm.Annotation.
* @param {Function} close Callback called when an annotation is closed.
* @param {Function} close Callback called when an annotation is closed. Passed a ve.dm.Annotation.
*/
ve.dm.Converter.openAndCloseAnnotations = function ( currentSet, targetSet, open, close ) {
var i, len, annotation, startClosingAt, currentSetOpen, targetSetOpen;
@ -87,7 +87,7 @@ ve.dm.Converter.openAndCloseAnnotations = function ( currentSet, targetSet, open
// Close all annotations from top to bottom (high to low)
// until we reach startClosingAt
for ( i = currentSet.getLength() - 1; i >= startClosingAt; i-- ) {
close();
close( currentSet.get( i ) );
// Remove from currentClone
currentSet.removeAt( i );
}
@ -294,9 +294,10 @@ ve.dm.Converter.prototype.canCloseWrapper = function () {
* @method
* @param {Object|Array} dataElement Linear model element or data slice
* @param {HTMLDocument} doc Document to create DOM elements in
* @param {HTMLElement[]} [childDomElements] Array of child DOM elements to pass in (annotations only)
* @returns {HTMLElement|boolean} DOM element, or false if the element cannot be converted
*/
ve.dm.Converter.prototype.getDomElementsFromDataElement = function ( dataElements, doc ) {
ve.dm.Converter.prototype.getDomElementsFromDataElement = function ( dataElements, doc, childDomElements ) {
var domElements,
dataElement = ve.isArray( dataElements ) ? dataElements[0] : dataElements,
nodeClass = this.modelRegistry.lookup( dataElement.type );
@ -307,8 +308,8 @@ ve.dm.Converter.prototype.getDomElementsFromDataElement = function ( dataElement
if ( nodeClass.static.isInternal ) {
return false;
}
domElements = nodeClass.static.toDomElements( dataElements, doc, this );
if ( !domElements || !domElements.length ) {
domElements = nodeClass.static.toDomElements( dataElements, doc, this, childDomElements );
if ( ( !domElements || !domElements.length ) && !( nodeClass.prototype instanceof ve.dm.Annotation ) ) {
throw new Error( 'toDomElements() failed to return an array when converting element of type ' + dataElement.type );
}
if ( dataElement.htmlAttributes ) {
@ -970,9 +971,9 @@ ve.dm.Converter.prototype.getDomFromData = function ( documentData, store, inter
* @throws Unbalanced data: looking for closing /type
*/
ve.dm.Converter.prototype.getDomSubtreeFromData = function ( data, container ) {
var text, i, j, annotations, annotationElement, dataElement, dataElementOrSlice,
var text, i, j, annotations, dataElement, dataElementOrSlice,
childDomElements, pre, ours, theirs, parentDomElement, lastChild, isContentNode, sibling,
previousSiblings, doUnwrap, textNode, type,
previousSiblings, doUnwrap, textNode, type, annotatedDomElementStack, annotatedDomElements,
dataLen = data.length,
canContainContentStack = [],
conv = this,
@ -980,28 +981,44 @@ ve.dm.Converter.prototype.getDomSubtreeFromData = function ( data, container ) {
domElement = container,
annotationStack = new ve.dm.AnnotationSet( this.store );
function openAnnotation( annotation ) {
// TODO this whole function should be rewritten with a domElementStack and ascend() and
// descend() functions, to build the whole DOM bottom-up rather than top-down. That would make
// unwrapping easier and will hopefully result in fewer DOM operations.
function openAnnotation() {
// Add text if needed
if ( text.length > 0 ) {
domElement.appendChild( doc.createTextNode( text ) );
annotatedDomElements.push( doc.createTextNode( text ) );
text = '';
}
// Create new node and descend into it
annotationElement = conv.getDomElementsFromDataElement(
annotation.getElement(), doc
)[0];
domElement.appendChild( annotationElement );
domElement = annotationElement;
annotatedDomElements = [];
annotatedDomElementStack.push( annotatedDomElements );
}
function closeAnnotation() {
function closeAnnotation( annotation ) {
var i, len, annotationElement, annotatedChildDomElements;
// Add text if needed
if ( text.length > 0 ) {
domElement.appendChild( doc.createTextNode( text ) );
annotatedDomElements.push( doc.createTextNode( text ) );
text = '';
}
// Traverse up
domElement = domElement.parentNode;
annotatedChildDomElements = annotatedDomElementStack.pop();
annotatedDomElements = annotatedDomElementStack[annotatedDomElementStack.length - 1];
annotationElement = conv.getDomElementsFromDataElement(
annotation.getElement(), doc, annotatedChildDomElements
)[0];
if ( annotationElement ) {
for ( i = 0, len = annotatedChildDomElements.length; i < len; i++ ) {
annotationElement.appendChild( annotatedChildDomElements[i] );
}
annotatedDomElements.push( annotationElement );
} else {
for ( i = 0, len = annotatedChildDomElements.length; i < len; i++ ) {
annotatedDomElements.push( annotatedChildDomElements[i] );
}
}
}
function findEndOfNode( i ) {
@ -1085,6 +1102,8 @@ ve.dm.Converter.prototype.getDomSubtreeFromData = function ( data, container ) {
) {
// Annotated text, nodes or meta
text = '';
annotatedDomElements = [];
annotatedDomElementStack = [ annotatedDomElements ];
while (
ve.isArray( data[i] ) ||
(
@ -1108,14 +1127,14 @@ ve.dm.Converter.prototype.getDomSubtreeFromData = function ( data, container ) {
// Annotated node
// Add text if needed
if ( text.length > 0 ) {
domElement.appendChild( doc.createTextNode( text ) );
annotatedDomElements.push( doc.createTextNode( text ) );
text = '';
}
// Insert the elements
dataElementOrSlice = getDataElementOrSlice();
childDomElements = this.getDomElementsFromDataElement( dataElementOrSlice, doc );
for ( j = 0; j < childDomElements.length; j++ ) {
domElement.appendChild( childDomElements[j] );
annotatedDomElements.push( childDomElements[j] );
}
if ( ve.isArray( dataElementOrSlice ) ) {
i += dataElementOrSlice.length - 1;
@ -1130,16 +1149,17 @@ ve.dm.Converter.prototype.getDomSubtreeFromData = function ( data, container ) {
// Add any gathered text
if ( text.length > 0 ) {
domElement.appendChild( doc.createTextNode( text ) );
annotatedDomElements.push( doc.createTextNode( text ) );
text = '';
}
// Close any remaining annotation nodes
for ( j = annotationStack.getLength() - 1; j >= 0; j-- ) {
// Traverse up
domElement = domElement.parentNode;
// Close any remaining annotations
ve.dm.Converter.openAndCloseAnnotations( annotationStack, new ve.dm.AnnotationSet(),
openAnnotation, closeAnnotation
);
// Put the annotated nodes in the DOM
for ( j = 0; j < annotatedDomElements.length; j++ ) {
domElement.appendChild( annotatedDomElements[j] );
}
// Clear annotationStack
annotationStack = new ve.dm.AnnotationSet( this.store );
} else if ( data[i].type !== undefined ) {
dataElement = data[i];
// Element

View file

@ -242,6 +242,21 @@ ve.dm.Model.matchesAttributeSpec = function ( attribute, spec ) {
return matchesArray( spec.whitelist || true ) && !matchesArray( spec.blacklist || false );
};
/**
* Get hash object of a linear model data element
*
* @static
* @param {Object} dataElement Data element
* @returns {Object} Hash object
*/
ve.dm.Model.static.getHashObject = function ( dataElement ) {
return {
type: dataElement.type,
attributes: dataElement.attributes,
htmlAttributes: dataElement.htmlAttributes
};
};
/* Methods */
/**
@ -357,3 +372,18 @@ ve.dm.Model.prototype.hasAttributes = function ( attributes, strict ) {
ve.dm.Model.prototype.getClonedElement = function () {
return ve.copyObject( this.element );
};
/**
* Get the hash object of the linear model element.
*
* The actual logic is in a static function as this needs
* to be accessible from ve.dm.Converter
*
* This is a custom hash function for ve#getHash.
*
* @method
* @returns {Object} Hash object
*/
ve.dm.Model.prototype.getHashObject = function () {
return this.constructor.static.getHashObject( this.element );
};

View file

@ -202,20 +202,6 @@ ve.dm.Node.static.remapStoreIndexes = function ( /*dataElement, mapping*/ ) {
ve.dm.Node.static.remapInternalListIndexes = function ( /*dataElement, mapping*/ ) {
};
/**
* Get hash object of a linear model data element
*
* @static
* @param {Object} dataElement Data element
* @returns {Object} Hash object
*/
ve.dm.Node.static.getHashObject = function ( dataElement ) {
return {
type: dataElement.type,
attributes: dataElement.attributes
};
};
/**
* Determine if a hybrid element is inline and allowed to be inline in this context
*
@ -561,18 +547,3 @@ ve.dm.Node.prototype.canBeMergedWith = function ( node ) {
}
return true;
};
/**
* Get the hash object of the node.
*
* The actual logic is in a static function as this needs
* to be accessible from ve.dm.Converter
*
* This is a custom hash function for ve#getHash.
*
* @method
* @returns {Object} Hash object
*/
ve.dm.Node.prototype.getHashObject = function () {
return this.constructor.static.getHashObject( this.element );
};