mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-12-01 17:36:35 +00:00
a379e0f91e
Follow up for I6a7c9e8ee8f995731bc205d666167874eb2ebe23 The first pass that Timo took missed the following cases * "{Array|String}": string is just one of the values * "{String[]}": string is followed by [] to indicate an array of strings Change-Id: I65e595e8d37fb624802d84af9536a2d3c5d73c7d
273 lines
8.3 KiB
JavaScript
273 lines
8.3 KiB
JavaScript
/*!
|
|
* VisualEditor AnnotationFactory class.
|
|
*
|
|
* @copyright 2011-2012 VisualEditor Team and others; see AUTHORS.txt
|
|
* @license The MIT License (MIT); see LICENSE.txt
|
|
*/
|
|
( function ( ve ) {
|
|
|
|
/**
|
|
* Factory for annotations.
|
|
*
|
|
* To register a new annotation type, call ve.dm.annotationFactory.register()
|
|
*
|
|
* @abstract
|
|
* @extends ve.EventEmitter
|
|
* @constructor
|
|
*/
|
|
ve.dm.AnnotationFactory = function () {
|
|
// Parent constructor
|
|
ve.Factory.call( this );
|
|
// [ { tagName: [annotationNamesWithoutFunc] }, { tagName: [annotationNamesWithFunc] } ]
|
|
this.annotationsByTag = [ {}, {} ];
|
|
// { matchFunctionPresence: { rdfaType: { tagName: [annotationNames] } } }
|
|
// [ { rdfaType: { tagName: [annotationNamesWithoutFunc] } }, { rdfaType: { tagName: [annotationNamesWithFunc] } ]
|
|
this.annotationsByTypeAndTag = [];
|
|
// { nameA: 0, nameB: 1, ... }
|
|
this.registrationOrder = {};
|
|
this.nextNumber = 0;
|
|
};
|
|
|
|
/* Inheritance */
|
|
|
|
ve.inheritClass( ve.dm.AnnotationFactory, ve.Factory );
|
|
|
|
/* Private helper functions */
|
|
|
|
/**
|
|
* Helper function for register(). Adds a value to the front of an array in a nested object.
|
|
* Objects and arrays are created if needed. You can specify either two or three keys and a value.
|
|
*
|
|
* Specifically:
|
|
* addType( obj, keyA, keyB, keyC, value ) does obj[keyA][keyB][keyC].unshift( value );
|
|
* addType( obj, keyA, keyB, value ) does obj[keyA][keyB].unshift( value );
|
|
*
|
|
* @param {Object} obj Object to add to
|
|
* @param {string} keyA Key into obj
|
|
* @param {string} keyB Key into obj[keyA]
|
|
* @param {string|Mixed} keyC Key into obj[keyA][keyB], or value to add to array if value not set
|
|
* @param {Mixed} [value] Value to add to the array
|
|
*/
|
|
function addType( obj, keyA, keyB, keyC, value ) {
|
|
if ( obj[keyA] === undefined ) {
|
|
obj[keyA] = {};
|
|
}
|
|
if ( obj[keyA][keyB] === undefined ) {
|
|
obj[keyA][keyB] = value === undefined ? [] : {};
|
|
}
|
|
if ( value !== undefined && obj[keyA][keyB][keyC] === undefined ) {
|
|
obj[keyA][keyB][keyC] = [];
|
|
}
|
|
if ( value === undefined ) {
|
|
obj[keyA][keyB].unshift( keyC );
|
|
} else {
|
|
obj[keyA][keyB][keyC].unshift( value );
|
|
}
|
|
}
|
|
|
|
/* Public methods */
|
|
|
|
/**
|
|
* Register an annotation type.
|
|
* @param {string} name Symbolic name for the annotation
|
|
* @param {ve.dm.Annotation} constructor Subclass of ve.dm.Annotation
|
|
*/
|
|
ve.dm.AnnotationFactory.prototype.register = function ( name, constructor ) {
|
|
if ( typeof name !== 'string' || name === '' ) {
|
|
throw new Error( 'Annotation names must be strings and must not be empty' );
|
|
}
|
|
// Call parent implementation
|
|
ve.Factory.prototype.register.call( this, name, constructor );
|
|
|
|
name = constructor.static.name;
|
|
|
|
var i, j,
|
|
tags = constructor.static.matchTagNames === null ?
|
|
[ '' ] :
|
|
constructor.static.matchTagNames,
|
|
types = constructor.static.matchRdfaTypes === null ?
|
|
[ '' ] :
|
|
constructor.static.matchRdfaTypes;
|
|
|
|
for ( i = 0; i < tags.length; i++ ) {
|
|
// +!!foo is a shorter equivalent of Number( Boolean( foo ) ) or foo ? 1 ; 0
|
|
addType( this.annotationsByTag, +!!constructor.static.matchFunction,
|
|
tags[i], name
|
|
);
|
|
}
|
|
for ( i = 0; i < types.length; i++ ) {
|
|
for ( j = 0; j < tags.length; j++ ) {
|
|
addType( this.annotationsByTypeAndTag,
|
|
+!!constructor.static.matchFunction, types[i], tags[j], name
|
|
);
|
|
}
|
|
}
|
|
|
|
this.registrationOrder[name] = this.nextNumber++;
|
|
};
|
|
|
|
/**
|
|
* Determine which annotation best matches the given element
|
|
*
|
|
* Annotation matching works as follows:
|
|
* Get all annotations whose tag and rdfaType rules match
|
|
* Rank them in order of specificity:
|
|
* * tag, rdfaType and func specified
|
|
* * rdfaType and func specified
|
|
* * tag and func specified
|
|
* * func specified
|
|
* * tag and rdfaType specified
|
|
* * rdfaType specified
|
|
* * tag specified
|
|
* * nothing specified
|
|
* If there are multiple candidates with the same specificity, they are ranked in reverse order of
|
|
* registration (i.e. if A was registered before B, B will rank above A).
|
|
* The highest-ranking annotation whose test function does not return false, wins.
|
|
*
|
|
* @param {HTMLElement} element Element to match
|
|
* @returns {string|null} Annotation type, or null if none found
|
|
*/
|
|
ve.dm.AnnotationFactory.prototype.matchElement = function ( element ) {
|
|
var i, name, ann, matches, winner, types,
|
|
tag = element.nodeName.toLowerCase(),
|
|
typeAttr = element.getAttribute( 'typeof' ) || element.getAttribute( 'rel' ),
|
|
reg = this;
|
|
|
|
function byRegistrationOrderDesc( a, b ) {
|
|
return reg.registrationOrder[b] - reg.registrationOrder[a];
|
|
}
|
|
|
|
function matchWithFunc( types, tag ) {
|
|
var i, j, matches, queue = [];
|
|
for ( i = 0; i < types.length; i++ ) {
|
|
matches = ve.getProp( reg.annotationsByTypeAndTag, 1, types[i], tag ) || [];
|
|
for ( j = 0; j < matches.length; j++ ) {
|
|
queue.push( matches[j] );
|
|
}
|
|
}
|
|
queue.sort( byRegistrationOrderDesc );
|
|
for ( i = 0; i < queue.length; i++ ) {
|
|
if ( reg.registry[queue[i]].static.matchFunction( element ) ) {
|
|
return queue[i];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function matchWithoutFunc( types, tag ) {
|
|
var i, j, matches, winningName = null;
|
|
for ( i = 0; i < types.length; i++ ) {
|
|
matches = ve.getProp( reg.annotationsByTypeAndTag, 0, types[i], tag ) || [];
|
|
for ( j = 0; j < matches.length; j++ ) {
|
|
if (
|
|
winningName === null ||
|
|
reg.registrationOrder[winningName] < reg.registrationOrder[matches[j]]
|
|
) {
|
|
winningName = matches[j];
|
|
}
|
|
}
|
|
}
|
|
return winningName;
|
|
}
|
|
|
|
types = typeAttr ? typeAttr.split( ' ' ) : [];
|
|
if ( types.length ) {
|
|
// func+tag+type match
|
|
winner = matchWithFunc( types, tag );
|
|
if ( winner !== null ) {
|
|
return winner;
|
|
}
|
|
|
|
// func+type match
|
|
// Only look at rules with no tag specified; if a rule does specify a tag, we've
|
|
// either already processed it above, or the tag doesn't match
|
|
winner = matchWithFunc( types, '' );
|
|
if ( winner !== null ) {
|
|
return winner;
|
|
}
|
|
}
|
|
|
|
// func+tag match
|
|
matches = ve.getProp( this.annotationsByTag, 1, tag ) || [];
|
|
// No need to sort because individual arrays in annotadtionsByTag are already sorted
|
|
// correctly
|
|
for ( i = 0; i < matches.length; i++ ) {
|
|
name = matches[i];
|
|
ann = this.registry[name];
|
|
// Only process this one if it doesn't specify types
|
|
// If it does specify types, then we've either already processed it in the
|
|
// func+tag+type step above, or its type rule doesn't match
|
|
if ( ann.static.matchRdfaTypes === null && ann.static.matchFunction( element ) ) {
|
|
return matches[i];
|
|
}
|
|
}
|
|
|
|
// func only
|
|
// We only need to get the [''][''] array because the other arrays were either
|
|
// already processed during the steps above, or have a type or tag rule that doesn't
|
|
// match this element.
|
|
// No need to sort because individual arrays in annotationsByTypeAndTag are already sorted
|
|
// correctly
|
|
matches = ve.getProp( this.annotationsByTypeAndTag, 1, '', '' ) || [];
|
|
for ( i = 0; i < matches.length; i++ ) {
|
|
if ( this.registry[matches[i]].static.matchFunction( element ) ) {
|
|
return matches[i];
|
|
}
|
|
}
|
|
|
|
// tag+type
|
|
winner = matchWithoutFunc( types, tag );
|
|
if ( winner !== null ) {
|
|
return winner;
|
|
}
|
|
|
|
// type only
|
|
// Only look at rules with no tag specified; if a rule does specify a tag, we've
|
|
// either already processed it above, or the tag doesn't match
|
|
winner = matchWithoutFunc( types, '' );
|
|
if ( winner !== null ) {
|
|
return winner;
|
|
}
|
|
|
|
// tag only
|
|
matches = ve.getProp( this.annotationsByTag, 0, tag ) || [];
|
|
// No need to track winningName because the individual arrays in annotationsByTag are
|
|
// already sorted correctly
|
|
for ( i = 0; i < matches.length; i++ ) {
|
|
name = matches[i];
|
|
ann = this.registry[name];
|
|
// Only process this one if it doesn't specify types
|
|
// If it does specify types, then we've either already processed it in the
|
|
// tag+type step above, or its type rule doesn't match
|
|
if ( ann.static.matchRdfaTypes === null ) {
|
|
return matches[i];
|
|
}
|
|
}
|
|
|
|
// Rules with no type or tag specified
|
|
// These are the only rules that can still qualify at this point, the others we've either
|
|
// already processed or have a type or tag rule that disqualifies them
|
|
matches = ve.getProp( this.annotationsByTypeAndTag, 0, '', '' ) || [];
|
|
if ( matches.length > 0 ) {
|
|
return matches[0];
|
|
}
|
|
|
|
// We didn't find anything, give up
|
|
return null;
|
|
};
|
|
|
|
ve.dm.AnnotationFactory.prototype.createFromElement = function ( element ) {
|
|
var name = this.matchElement( element );
|
|
if ( name === null ) {
|
|
return null;
|
|
} else {
|
|
return this.create( name, element );
|
|
}
|
|
};
|
|
|
|
/* Initialization */
|
|
|
|
ve.dm.annotationFactory = new ve.dm.AnnotationFactory();
|
|
|
|
} )( ve );
|