mediawiki-extensions-Visual.../modules/ve-mw/dm/models/ve.dm.MWTransclusionModel.js
Ed Sanders c70d32c017 Use registered target, instead of ve.init.Target
Depends on I468d4eb4 in core.

Uses various hacks to trick the test runner into thinking
an MW target is in use, when in fact we still use SA targets.

Change-Id: If4611307d5d7aaee4af84f86ef82faf9078043b6
2014-06-17 20:41:01 +01:00

480 lines
12 KiB
JavaScript

/*!
* VisualEditor DataModel MWTransclusionModel class.
*
* @copyright 2011-2014 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/*global mw */
( function () {
var hasOwn = Object.hasOwnProperty,
specCache = {};
/**
* MediaWiki transclusion model.
*
* @class
* @mixins OO.EventEmitter
*
* @constructor
*/
ve.dm.MWTransclusionModel = function VeDmMWTransclusionModel() {
// Mixin constructors
OO.EventEmitter.call( this );
// Properties
this.parts = [];
this.uid = 0;
this.requests = [];
this.queue = [];
};
/* Inheritance */
OO.mixinClass( ve.dm.MWTransclusionModel, OO.EventEmitter );
/* Events */
/**
* @event replace
* @param {ve.dm.MWTransclusionPartModel|null} removed Removed part
* @param {ve.dm.MWTransclusionPartModel|null} added Added part
*/
/**
* @event change
*/
/* Methods */
/**
* Insert transclusion at the end of a surface fragment.
*
* @param {ve.dm.SurfaceFragment} surfaceModel Surface fragment to insert at
*/
ve.dm.MWTransclusionModel.prototype.insertTransclusionNode = function ( surfaceFragment ) {
surfaceFragment
.collapseRangeToEnd()
.insertContent( [
{
'type': 'mwTransclusionInline',
'attributes': {
'mw': this.getPlainObject()
}
},
{ 'type': '/mwTransclusionInline' }
] );
};
/**
* Update transclusion node in a document.
*
* @param {ve.dm.Surface} surfaceModel Surface model of main document
* @param {ve.dm.MWTransclusionNode} node Transclusion node to update
*/
ve.dm.MWTransclusionModel.prototype.updateTransclusionNode = function ( surfaceModel, node ) {
var obj = this.getPlainObject();
if ( obj !== null ) {
surfaceModel.getFragment( node.getOuterRange(), true )
.changeAttributes( { 'mw': obj } );
} else {
surfaceModel.getFragment( node.getOuterRange(), true )
.removeContent();
}
};
/**
* Load from transclusion data, and fetch spec from server.
*
* @param {Object} data Transclusion data
* @returns {jQuery.Promise} Promise, resolved when spec is loaded
*/
ve.dm.MWTransclusionModel.prototype.load = function ( data ) {
var i, len, part, deferred,
promises = [];
// Convert single part format to multi-part format
// Parsoid doesn't use this format any more, but we accept it for backwards compatibility
if ( data.params && data.target ) {
data = { 'parts': [ { 'template': data } ] };
}
if ( ve.isArray( data.parts ) ) {
for ( i = 0, len = data.parts.length; i < len; i++ ) {
part = data.parts[i];
if ( part.template ) {
deferred = $.Deferred();
promises.push( deferred.promise() );
this.queue.push( {
'add': ve.dm.MWTemplateModel.newFromData( this, part.template ),
'deferred': deferred
} );
} else if ( typeof part === 'string' ) {
deferred = $.Deferred();
promises.push( deferred.promise() );
this.queue.push( {
'add': new ve.dm.MWTransclusionContentModel( this, part, 'data' ),
'deferred': deferred
} );
}
}
setTimeout( ve.bind( this.fetch, this ) );
}
return $.when.apply( $, promises );
};
/**
* Process one or more queue items.
*
* @param {Object[]} queue List of objects containing parts to add and optionally indexes to add
* them at, if no index is given parts will be added at the end
* @fires replace For each item added
* @fires change
*/
ve.dm.MWTransclusionModel.prototype.process = function ( queue ) {
var i, len, item, title, index, remove, existing;
for ( i = 0, len = queue.length; i < len; i++ ) {
remove = 0;
item = queue[i];
if ( item.add instanceof ve.dm.MWTemplateModel ) {
title = item.add.getTitle();
if ( hasOwn.call( specCache, title ) && specCache[title] ) {
item.add.getSpec().extend( specCache[title] );
}
}
// Use specified index
index = item.index;
// Auto-remove if already existing, preserving index
existing = ve.indexOf( item.add, this.parts );
if ( existing !== -1 ) {
this.removePart( item.add );
if ( index && existing + 1 < index ) {
index--;
}
}
// Derive index from removal if given
if ( index === undefined && item.remove ) {
index = ve.indexOf( item.remove, this.parts );
if ( index !== -1 ) {
remove = 1;
}
}
// Use last index as a last resort
if ( index === undefined || index === -1 ) {
index = this.parts.length;
}
this.parts.splice( index, remove, item.add );
if ( item.add ) {
item.add.connect( this, { 'change': [ 'emit', 'change' ] } );
}
if ( item.remove ) {
item.remove.disconnect( this );
}
this.emit( 'replace', item.remove || null, item.add );
// Resolve promises
if ( item.deferred ) {
item.deferred.resolve();
}
}
this.emit( 'change' );
};
/** */
ve.dm.MWTransclusionModel.prototype.fetch = function () {
if ( !this.queue.length ) {
return;
}
var i, len, item, title, request,
titles = [],
specs = {},
queue = this.queue.slice();
// Clear shared queue for future calls
this.queue.length = 0;
// Get unique list of template titles that aren't already loaded
for ( i = 0, len = queue.length; i < len; i++ ) {
item = queue[i];
if ( item.add instanceof ve.dm.MWTemplateModel ) {
title = item.add.getTitle();
if (
// Skip titles that don't have a resolvable href
title &&
// Skip titles outside the template namespace
title.charAt( 0 ) !== ':' &&
// Skip already cached data
!hasOwn.call( specCache, title ) &&
// Skip duplicate titles in the same batch
ve.indexOf( title, titles ) === -1
) {
titles.push( title );
}
}
}
// Bypass server for empty lists
if ( !titles.length ) {
setTimeout( ve.bind( this.process, this, queue ) );
return;
}
// Request template specs from server
request = ve.init.target.constructor.static.apiRequest( {
'action': 'templatedata',
'titles': titles.join( '|' ),
'lang': mw.config.get( 'wgUserLanguage' ),
'redirects': '1'
} )
.done( function ( data ) {
var i, len, id, aliasMap = [];
if ( data && data.pages ) {
// Keep spec data on hand for future use
for ( id in data.pages ) {
specs[data.pages[id].title] = data.pages[id];
}
// Follow redirects
if ( data.redirects ) {
aliasMap = data.redirects;
}
// Follow MW's normalisation
if ( data.normalized ) {
aliasMap.push.apply( aliasMap, data.normalized );
}
// Cross-reference aliased titles.
for ( i = 0, len = aliasMap.length; i < len; i++ ) {
// Only define the alias if the target exists, otherwise
// we create a new property with an invalid "undefined" value.
if ( hasOwn.call( specs, aliasMap[i].to ) ) {
specs[aliasMap[i].from] = specs[aliasMap[i].to];
}
}
// Prevent asking again for templates that have no specs
for ( i = 0, len = titles.length; i < len; i++ ) {
title = titles[i];
if ( !specs[title] ) {
specs[title] = null;
}
}
ve.extendObject( specCache, specs );
}
} )
.always( ve.bind( function () {
// Prune completed request
var index = ve.indexOf( request, this.requests );
if ( index !== -1 ) {
this.requests.splice( index, 1 );
}
// Actually add queued items
this.process( queue );
}, this ) );
this.requests.push( request );
};
/**
* Abort any pending requests.
*/
ve.dm.MWTransclusionModel.prototype.abortRequests = function () {
var i, len;
for ( i = 0, len = this.requests.length; i < len; i++ ) {
this.requests[i].abort();
}
this.requests.length = 0;
};
/**
* Get plain object representation of template transclusion.
*
* @returns {Object|null} Plain object representation, or null if empty
*/
ve.dm.MWTransclusionModel.prototype.getPlainObject = function () {
var i, len, part, serialization,
obj = { 'parts': [] };
for ( i = 0, len = this.parts.length; i < len; i++ ) {
part = this.parts[i];
serialization = part.serialize();
if ( serialization !== undefined ) {
obj.parts.push( serialization );
}
}
if ( obj.parts.length === 0 ) {
return null;
}
return obj;
};
/**
* Get the wikitext for this transclusion.
*
* @returns {string} Wikitext like `{{foo|1=bar|baz=quux}}`
*/
ve.dm.MWTransclusionModel.prototype.getWikitext = function () {
var i, len,
wikitext = '';
for ( i = 0, len = this.parts.length; i < len; i++ ) {
wikitext += this.parts[i].getWikitext();
}
return wikitext;
};
/**
* Get a unique ID for a part in the transclusion.
*
* This is used to give parts unique IDs, and returns a different value each time it's called.
*
* @returns {number} Unique ID
*/
ve.dm.MWTransclusionModel.prototype.getUniquePartId = function () {
return this.uid++;
};
/**
* Replace part.
*
* Replace asynchonously.
*
* @param {ve.dm.MWTransclusionPartModel} remove Part to remove
* @param {ve.dm.MWTransclusionPartModel} add Part to add
* @throws {Error} If part to remove is not valid
* @throws {Error} If part to add is not valid
* @returns {jQuery.Promise} Promise, resolved when part is added
*/
ve.dm.MWTransclusionModel.prototype.replacePart = function ( remove, add ) {
var deferred = $.Deferred();
if (
!( remove instanceof ve.dm.MWTransclusionPartModel ) ||
!( add instanceof ve.dm.MWTransclusionPartModel )
) {
throw new Error( 'Invalid transclusion part' );
}
this.queue.push( { 'remove': remove, 'add': add, 'deferred': deferred } );
// Fetch on next yield to process items in the queue together, subsequent calls to fetch will
// have no effect because the queue will be clear
setTimeout( ve.bind( this.fetch, this ) );
return deferred.promise();
};
/**
* Add part.
*
* Added asynchronously, but order is preserved.
*
* @param {ve.dm.MWTransclusionPartModel} part Part to add
* @param {number} [index] Specific index to add content at, defaults to the end
* @throws {Error} If part is not valid
* @returns {jQuery.Promise} Promise, resolved when part is added
*/
ve.dm.MWTransclusionModel.prototype.addPart = function ( part, index ) {
var deferred = $.Deferred();
if ( !( part instanceof ve.dm.MWTransclusionPartModel ) ) {
throw new Error( 'Invalid transclusion part' );
}
this.queue.push( { 'add': part, 'index': index, 'deferred': deferred } );
// Fetch on next yield to process items in the queue together, subsequent calls to fetch will
// have no effect because the queue will be clear
setTimeout( ve.bind( this.fetch, this ) );
return deferred.promise();
};
/**
* Remove a part.
*
* @param {ve.dm.MWTransclusionPartModel} part Part to remove
* @fires replace
*/
ve.dm.MWTransclusionModel.prototype.removePart = function ( part ) {
var index = ve.indexOf( part, this.parts );
if ( index !== -1 ) {
this.parts.splice( index, 1 );
part.disconnect( this );
this.emit( 'replace', part, null );
}
};
/**
* Get all parts.
*
* @returns {ve.dm.MWTransclusionPartModel[]} Parts in transclusion
*/
ve.dm.MWTransclusionModel.prototype.getParts = function () {
return this.parts;
};
/**
* Get part by its ID.
*
* Matching is performed against the first section of the `id`, delimited by a '/'.
*
* @param {string} id Part ID
* @returns {ve.dm.MWTransclusionPartModel|null} Part with matching ID, if found
*/
ve.dm.MWTransclusionModel.prototype.getPartFromId = function ( id ) {
var i, len,
// For ids from ve.dm.MWParameterModel, compare against the part id
// of the parameter instead of the entire model id (e.g. "part_1" instead of "part_1/foo").
partId = id.split( '/' )[0];
for ( i = 0, len = this.parts.length; i < len; i++ ) {
if ( this.parts[i].getId() === partId ) {
return this.parts[i];
}
}
return null;
};
/**
* Get the index of a part or parameter.
*
* Indexes are linear depth-first addresses in the transclusion tree.
*
* @param {ve.dm.MWTransclusionPartModel|ve.dm.MWParameterModel} model Part or parameter
* @returns {number} Page index of model
*/
ve.dm.MWTransclusionModel.prototype.getIndex = function ( model ) {
var i, iLen, j, jLen, part, names,
parts = this.parts,
index = 0;
for ( i = 0, iLen = parts.length; i < iLen; i++ ) {
part = parts[i];
if ( part === model ) {
return index;
}
index++;
if ( part instanceof ve.dm.MWTemplateModel ) {
names = part.getParameterNames();
for ( j = 0, jLen = names.length; j < jLen; j++ ) {
if ( part.getParameter( names[j] ) === model ) {
return index;
}
index++;
}
}
}
return -1;
};
}() );