[BREAKING CHANGE] Evalute block/inline state when inserting a transclusion node

Make some of the methods we currently use to render the node
static so we can re-use them before inserting. We do the evaluation
without inserting the node so as not to dirty the document and
transcation history.

In the unlikely case the request fails, just fallback to inline.

This only handles insertions for now as type changes on edit will be
very rare.

This changes the signature of insertTransclusionNode, which is used
in Cite and Citoid extensions.

Bug: T51784
Change-Id: Ibc2fc66e6866084b0a4deeb082c8a1ca412febb2
This commit is contained in:
Ed Sanders 2016-05-15 12:08:13 +01:00 committed by James D. Forrester
parent 3d7352b5ec
commit 236e3d1241
4 changed files with 130 additions and 60 deletions

View file

@ -87,6 +87,33 @@ ve.ce.MWTransclusionNode.static.getDescription = function ( model ) {
.join( ve.msg( 'comma-separator' ) );
};
/**
* Filter rendering to remove auto-generated content and wrappers
*
* @static
* @param {HTMLElement[]} contentNodes Rendered nodes
* @return {HTMLElement[]} Filtered rendered nodes
*/
ve.ce.MWTransclusionNode.static.filterRendering = function ( contentNodes ) {
// Filter out auto-generated items, e.g. reference lists
contentNodes = contentNodes.filter( function ( node ) {
var dataMw = node.nodeType === Node.ELEMENT_NODE &&
node.hasAttribute( 'data-mw' ) &&
JSON.parse( node.getAttribute( 'data-mw' ) );
if ( dataMw && dataMw.autoGenerated ) {
return false;
}
return true;
} );
// HACK: if $content consists of a single paragraph, unwrap it.
// We have to do this because the parser wraps everything in <p>s, and inline templates
// will render strangely when wrapped in <p>s.
if ( contentNodes.length === 1 && contentNodes[ 0 ].nodeName.toLowerCase() === 'p' ) {
contentNodes = Array.prototype.slice.apply( contentNodes[ 0 ].childNodes );
}
return contentNodes;
};
/* Methods */
/**
@ -122,24 +149,7 @@ ve.ce.MWTransclusionNode.prototype.onParseSuccess = function ( deferred, respons
// Work around https://github.com/jquery/jquery/issues/1997
contentNodes = $.parseHTML( response.visualeditor.content, this.getModelHtmlDocument() ) || [];
// Filter out auto-generated items, e.g. reference lists
contentNodes = contentNodes.filter( function ( node ) {
var dataMw = node.nodeType === Node.ELEMENT_NODE &&
node.hasAttribute( 'data-mw' ) &&
JSON.parse( node.getAttribute( 'data-mw' ) );
if ( dataMw && dataMw.autoGenerated ) {
return false;
}
return true;
} );
// HACK: if $content consists of a single paragraph, unwrap it.
// We have to do this because the parser wraps everything in <p>s, and inline templates
// will render strangely when wrapped in <p>s.
if ( contentNodes.length === 1 && contentNodes[ 0 ].nodeName.toLowerCase() === 'p' ) {
contentNodes = Array.prototype.slice.apply( contentNodes[ 0 ].childNodes );
}
deferred.resolve( contentNodes );
deferred.resolve( this.constructor.static.filterRendering( contentNodes ) );
};
/**

View file

@ -50,19 +50,63 @@
/**
* Insert transclusion at the end of a surface fragment.
*
* @param {ve.dm.SurfaceFragment} surfaceFragment Surface fragment to insert at
* If forceType is not specified and this is used in async mode, users of this method
* should ensure the surface is not accessible while the type is being evaluated.
*
* @param {ve.dm.SurfaceFragment} surfaceFragment Surface fragment after which to insert.
* @param {boolean|undefined} [forceType] Force the type to 'inline' or 'block'. If not
* specified it will be evaluated asynchronously.
* @return {jQuery.Promise} Promise which resolves when the node has been inserted. If
* forceType was specified this will be instant.
*/
ve.dm.MWTransclusionModel.prototype.insertTransclusionNode = function ( surfaceFragment ) {
surfaceFragment
.insertContent( [
{
type: 'mwTransclusionInline',
attributes: {
mw: this.getPlainObject()
ve.dm.MWTransclusionModel.prototype.insertTransclusionNode = function ( surfaceFragment, forceType ) {
var model = this,
deferred = $.Deferred(),
nodeClass = ve.dm.MWTransclusionNode;
function insertNode( isInline ) {
var type = isInline ? nodeClass.static.inlineType : nodeClass.static.blockType;
surfaceFragment
.insertContent( [
{
type: type,
attributes: {
mw: model.getPlainObject()
}
},
{ type: '/' + type }
] );
deferred.resolve();
}
if ( forceType ) {
insertNode( forceType === 'inline' );
} else {
new mw.Api().post( {
action: 'visualeditor',
paction: 'parsefragment',
page: mw.config.get( 'wgRelevantPageName' ),
wikitext: nodeClass.static.getWikitext( this.getPlainObject() ),
pst: 1
} )
.then( function ( response ) {
var contentNodes;
if ( ve.getProp( response, 'visualeditor', 'result' ) === 'success' ) {
contentNodes = $.parseHTML( response.visualeditor.content, surfaceFragment.getDocument().getHtmlDocument() ) || [];
contentNodes = ve.ce.MWTransclusionNode.static.filterRendering( contentNodes );
insertNode(
nodeClass.static.isHybridInline( contentNodes, ve.dm.converter )
);
} else {
// Request failed - just assume inline
insertNode( true );
}
},
{ type: '/mwTransclusionInline' }
] );
}, function () {
insertNode( true );
} );
}
return deferred.promise();
};
/**

View file

@ -268,6 +268,41 @@ ve.dm.MWTransclusionNode.static.escapeParameter = function ( param ) {
return output;
};
/**
* Get the wikitext for this transclusion.
*
* @static
* @param {Object} content MW data content
* @return {string} Wikitext like `{{foo|1=bar|baz=quux}}`
*/
ve.dm.MWTransclusionNode.static.getWikitext = function ( content ) {
var i, len, part, template, param,
wikitext = '';
// Normalize to multi template format
if ( content.params ) {
content = { parts: [ { template: content } ] };
}
// Build wikitext from content
for ( i = 0, len = content.parts.length; i < len; i++ ) {
part = content.parts[ i ];
if ( part.template ) {
// Template
template = part.template;
wikitext += '{{' + template.target.wt;
for ( param in template.params ) {
wikitext += '|' + param + '=' +
this.escapeParameter( template.params[ param ].wt );
}
wikitext += '}}';
} else {
// Plain wikitext
wikitext += part;
}
}
return wikitext;
};
/* Methods */
/**
@ -350,38 +385,13 @@ ve.dm.MWTransclusionNode.prototype.getPartsList = function () {
};
/**
* Get the wikitext for this transclusion.
* Wrapper for static method
*
* @method
* @return {string} Wikitext like `{{foo|1=bar|baz=quux}}`
*/
ve.dm.MWTransclusionNode.prototype.getWikitext = function () {
var i, len, part, template, param,
content = this.getAttribute( 'mw' ),
wikitext = '';
// Normalize to multi template format
if ( content.params ) {
content = { parts: [ { template: content } ] };
}
// Build wikitext from content
for ( i = 0, len = content.parts.length; i < len; i++ ) {
part = content.parts[ i ];
if ( part.template ) {
// Template
template = part.template;
wikitext += '{{' + template.target.wt;
for ( param in template.params ) {
wikitext += '|' + param + '=' +
this.constructor.static.escapeParameter( template.params[ param ].wt );
}
wikitext += '}}';
} else {
// Plain wikitext
wikitext += part;
}
}
return wikitext;
return this.constructor.static.getWikitext( this.getAttribute( 'mw' ) );
};
/* Concrete subclasses */

View file

@ -414,19 +414,25 @@ ve.ui.MWTemplateDialog.prototype.getActionProcess = function ( action ) {
return new OO.ui.Process( function () {
var deferred = $.Deferred();
dialog.checkRequiredParameters().done( function () {
var surfaceModel = dialog.getFragment().getSurface(),
var modelPromise,
surfaceModel = dialog.getFragment().getSurface(),
obj = dialog.transclusionModel.getPlainObject();
dialog.pushPending();
if ( dialog.selectedNode instanceof ve.dm.MWTransclusionNode ) {
dialog.transclusionModel.updateTransclusionNode( surfaceModel, dialog.selectedNode );
// TODO: updating the node could result in the inline/block state change
modelPromise = $.Deferred().resolve().promise();
} else if ( obj !== null ) {
// Collapse returns a new fragment, so update dialog.fragment
dialog.fragment = dialog.getFragment().collapseToEnd();
dialog.transclusionModel.insertTransclusionNode( dialog.getFragment() );
modelPromise = dialog.transclusionModel.insertTransclusionNode( dialog.getFragment() );
}
dialog.pushPending();
dialog.close( { action: action } ).always( dialog.popPending.bind( dialog ) );
return modelPromise.then( function () {
dialog.close( { action: action } ).always( dialog.popPending.bind( dialog ) );
} );
} ).always( deferred.resolve );
return deferred;