mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-12-01 09:26:37 +00:00
Add extension-specific types functionality to ModelRegistry
Extension-specific types are RDFa types (or type regexes) that are registered with the ModelRegistry separately. If an element has a type that is extension-specific, then that element can only be matched by a rule that asserts one of its extension-specific types. For MediaWiki, we would call ve.dm.modelRegistry.registerExtensionSpecificType(/^mw:/ ) . So then an element like <span typeof="mw:foobar"> would either match a rule specifically for mw:foobar, if one exists, or no rule at all; even the rule for <span> would not match. The consequence of this is that elements with unrecognized mw:-prefixed RDFa types are alienated. Change-Id: Ia8ab1fe5dffb9f813689324372a168e8e4a3e0bc
This commit is contained in:
parent
99df776543
commit
d73d6e9bf0
|
@ -25,6 +25,7 @@ ve.dm.ModelRegistry = function VeDmModelRegistry() {
|
|||
// { nameA: 0, nameB: 1, ... }
|
||||
this.registrationOrder = {};
|
||||
this.nextNumber = 0;
|
||||
this.extSpecificTypes = [];
|
||||
};
|
||||
|
||||
/* Inheritance */
|
||||
|
@ -82,9 +83,6 @@ ve.dm.ModelRegistry.prototype.register = function ( constructor ) {
|
|||
ve.dm.annotationFactory.register( name, constructor );
|
||||
} else if ( constructor.prototype instanceof ve.dm.Node ) {
|
||||
ve.dm.nodeFactory.register( name, constructor );
|
||||
// TODO handle things properly so we don't need this
|
||||
// HACK don't actually register nodes here, the converter isn't ready for that yet
|
||||
return;
|
||||
} else {
|
||||
throw new Error( 'Models must be subclasses of ve.dm.Annotation or ve.dm.Node' );
|
||||
}
|
||||
|
@ -115,6 +113,38 @@ ve.dm.ModelRegistry.prototype.register = function ( constructor ) {
|
|||
this.registrationOrder[name] = this.nextNumber++;
|
||||
};
|
||||
|
||||
/**
|
||||
* Register an extension-specific RDFa type or set of types. Unrecognized extension-specific types
|
||||
* skip non-type matches and are alienated.
|
||||
*
|
||||
* If a DOM node has RDFa types that are extension-specific, any matches that do not involve one of
|
||||
* those extension-specific types will be ignored. This means that if 'bar' is an
|
||||
* extension-specific type, and there are no models specifying 'bar' in their .matchRdfaTypes, then
|
||||
* <foo typeof="bar baz"> will not match anything, not even a model with .matchTagNames=['foo']
|
||||
* or one with .matchRdfaTypes=['baz'] .
|
||||
*
|
||||
* @param {string|RegExp} type Type, or regex matching types, to designate as extension-specifics
|
||||
*/
|
||||
ve.dm.ModelRegistry.prototype.registerExtensionSpecificType = function ( type ) {
|
||||
this.extSpecificTypes.push( type );
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks whether a given type matches one of the registered extension-specific types.
|
||||
* @param {string} type Type to check
|
||||
* @returns {boolean} Whether type is extension-specific
|
||||
*/
|
||||
ve.dm.ModelRegistry.prototype.isExtensionSpecificType = function ( type ) {
|
||||
var i, len, t;
|
||||
for ( i = 0, len = this.extSpecificTypes.length; i < len; i++ ) {
|
||||
t = this.extSpecificTypes[i];
|
||||
if ( t === type || ( t instanceof RegExp && type.match( t ) ) ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine which model best matches the given element
|
||||
*
|
||||
|
@ -137,7 +167,7 @@ ve.dm.ModelRegistry.prototype.register = function ( constructor ) {
|
|||
* @returns {string|null} Model type, or null if none found
|
||||
*/
|
||||
ve.dm.ModelRegistry.prototype.matchElement = function ( element ) {
|
||||
var i, name, model, matches, winner, types,
|
||||
var i, name, model, matches, winner, types, elementExtSpecificTypes, matchTypes,
|
||||
tag = element.nodeName.toLowerCase(),
|
||||
typeAttr = element.getAttribute( 'typeof' ) || element.getAttribute( 'rel' ),
|
||||
reg = this;
|
||||
|
@ -180,9 +210,13 @@ ve.dm.ModelRegistry.prototype.matchElement = function ( element ) {
|
|||
}
|
||||
|
||||
types = typeAttr ? typeAttr.split( ' ' ) : [];
|
||||
elementExtSpecificTypes = ve.filterArray( types, ve.bind( this.isExtensionSpecificType, this ) );
|
||||
// If the element has extension-specific types, only use those for matching and ignore its
|
||||
// other types. If it has no extension-specific types, use all of its types.
|
||||
matchTypes = elementExtSpecificTypes.length === 0 ? types : elementExtSpecificTypes;
|
||||
if ( types.length ) {
|
||||
// func+tag+type match
|
||||
winner = matchWithFunc( types, tag );
|
||||
winner = matchWithFunc( matchTypes, tag );
|
||||
if ( winner !== null ) {
|
||||
return winner;
|
||||
}
|
||||
|
@ -190,42 +224,45 @@ ve.dm.ModelRegistry.prototype.matchElement = function ( element ) {
|
|||
// 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, '' );
|
||||
winner = matchWithFunc( matchTypes, '' );
|
||||
if ( winner !== null ) {
|
||||
return winner;
|
||||
}
|
||||
}
|
||||
|
||||
// func+tag match
|
||||
matches = ve.getProp( this.modelsByTag, 1, tag ) || [];
|
||||
// No need to sort because individual arrays in modelsByTag are already sorted
|
||||
// correctly
|
||||
for ( i = 0; i < matches.length; i++ ) {
|
||||
name = matches[i];
|
||||
model = 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 ( model.static.matchRdfaTypes === null && model.static.matchFunction( element ) ) {
|
||||
return matches[i];
|
||||
// Do not check for type-less matches if the element has extension-specific types
|
||||
if ( elementExtSpecificTypes.length === 0 ) {
|
||||
// func+tag match
|
||||
matches = ve.getProp( this.modelsByTag, 1, tag ) || [];
|
||||
// No need to sort because individual arrays in modelsByTag are already sorted
|
||||
// correctly
|
||||
for ( i = 0; i < matches.length; i++ ) {
|
||||
name = matches[i];
|
||||
model = 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 ( model.static.matchRdfaTypes === null && model.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 modelsByTypeAndTag are already sorted
|
||||
// correctly
|
||||
matches = ve.getProp( this.modelsByTypeAndTag, 1, '', '' ) || [];
|
||||
for ( i = 0; i < matches.length; i++ ) {
|
||||
if ( this.registry[matches[i]].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 modelsByTypeAndTag are already sorted
|
||||
// correctly
|
||||
matches = ve.getProp( this.modelsByTypeAndTag, 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 );
|
||||
winner = matchWithoutFunc( matchTypes, tag );
|
||||
if ( winner !== null ) {
|
||||
return winner;
|
||||
}
|
||||
|
@ -233,14 +270,20 @@ ve.dm.ModelRegistry.prototype.matchElement = function ( element ) {
|
|||
// 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, '' );
|
||||
winner = matchWithoutFunc( matchTypes, '' );
|
||||
if ( winner !== null ) {
|
||||
return winner;
|
||||
}
|
||||
|
||||
if ( elementExtSpecificTypes.length > 0 ) {
|
||||
// There are only type-less matches beyond this point, so if we have any
|
||||
// extension-specific types, we give up now.
|
||||
return null;
|
||||
}
|
||||
|
||||
// tag only
|
||||
matches = ve.getProp( this.modelsByTag, 0, tag ) || [];
|
||||
// No need to track winningName because the individual arrays in nodelsByTag are
|
||||
// No need to track winningName because the individual arrays in modelsByTag are
|
||||
// already sorted correctly
|
||||
for ( i = 0; i < matches.length; i++ ) {
|
||||
name = matches[i];
|
||||
|
|
|
@ -72,9 +72,20 @@ ve.dm.StubSingleTagAndTypeAndFuncAnnotation.static.matchTagNames = ['a'];
|
|||
ve.dm.StubSingleTagAndTypeAndFuncAnnotation.static.matchRdfaTypes = ['mw:foo'];
|
||||
ve.dm.StubSingleTagAndTypeAndFuncAnnotation.static.matchFunction = checkForPickMe;
|
||||
|
||||
ve.dm.StubBarNode = function VeDmStubBarNode( children, element ) {
|
||||
ve.dm.BranchNode.call( this, 'stub-bar', children, element );
|
||||
};
|
||||
ve.inheritClass( ve.dm.StubBarNode, ve.dm.BranchNode );
|
||||
ve.dm.StubBarNode.static.name = 'stub-bar';
|
||||
ve.dm.StubBarNode.static.matchRdfaTypes = ['bar'];
|
||||
// HACK keep ve.dm.Converter happy for now
|
||||
// TODO once ve.dm.Converter is rewritten, this can be removed
|
||||
ve.dm.StubBarNode.static.toDataElement = function () {};
|
||||
ve.dm.StubBarNode.static.toDomElement = function () {};
|
||||
|
||||
/* Tests */
|
||||
|
||||
QUnit.test( 'matchElement', 9, function ( assert ) {
|
||||
QUnit.test( 'matchElement', 16, function ( assert ) {
|
||||
var registry = new ve.dm.ModelRegistry(), element;
|
||||
element = document.createElement( 'a' );
|
||||
assert.deepEqual( registry.matchElement( element ), null, 'matchElement() returns null if registry empty' );
|
||||
|
@ -87,6 +98,7 @@ QUnit.test( 'matchElement', 9, function ( assert ) {
|
|||
registry.register( ve.dm.StubSingleTagAndFuncAnnotation );
|
||||
registry.register( ve.dm.StubSingleTypeAndFuncAnnotation );
|
||||
registry.register( ve.dm.StubSingleTagAndTypeAndFuncAnnotation );
|
||||
registry.register( ve.dm.StubBarNode );
|
||||
|
||||
element = document.createElement( 'b' );
|
||||
assert.deepEqual( registry.matchElement( element ), 'stubnothingset', 'nothingset matches anything' );
|
||||
|
@ -105,4 +117,22 @@ QUnit.test( 'matchElement', 9, function ( assert ) {
|
|||
assert.deepEqual( registry.matchElement( element ), 'stubfunc', 'func-only match' );
|
||||
element.setAttribute( 'rel', 'mw:foo' );
|
||||
assert.deepEqual( registry.matchElement( element ), 'stubsingletypeandfunc', 'type and func match' );
|
||||
|
||||
registry.registerExtensionSpecificType( /^mw:/ );
|
||||
registry.registerExtensionSpecificType( 'foo' );
|
||||
element = document.createElement( 'a' );
|
||||
element.setAttribute( 'rel', 'bar baz' );
|
||||
assert.deepEqual( registry.matchElement( element ), 'stub-bar', 'incomplete non-extension-specific type match' );
|
||||
element.setAttribute( 'pickme', 'true' );
|
||||
assert.deepEqual( registry.matchElement( element ), 'stubsingletagandfunc', 'incomplete non-extension-specific type match is trumped by tag&func match' );
|
||||
element.setAttribute( 'rel', 'mw:bogus' );
|
||||
assert.deepEqual( registry.matchElement( element ), null, 'extension-specific type matching regex prevents tag-only and func-only matches' );
|
||||
element.setAttribute( 'rel', 'foo' );
|
||||
assert.deepEqual( registry.matchElement( element ), null, 'extension-specific type matching string prevents tag-only and func-only matches' );
|
||||
element.setAttribute( 'rel', 'mw:bogus bar' );
|
||||
assert.deepEqual( registry.matchElement( element ), null, 'extension-specific type matching regex prevents type match' );
|
||||
element.setAttribute( 'rel', 'foo bar' );
|
||||
assert.deepEqual( registry.matchElement( element ), null, 'extension-specific type matching string prevents type match' );
|
||||
element.setAttribute( 'rel', 'foo bar mw:bogus' );
|
||||
assert.deepEqual( registry.matchElement( element ), null, 'two extension-specific types prevent non-extension-specific type match' );
|
||||
} );
|
||||
|
|
Loading…
Reference in a new issue