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:
Catrope 2013-01-22 14:50:35 -08:00
parent 99df776543
commit d73d6e9bf0
2 changed files with 106 additions and 33 deletions

View file

@ -25,6 +25,7 @@ ve.dm.ModelRegistry = function VeDmModelRegistry() {
// { nameA: 0, nameB: 1, ... } // { nameA: 0, nameB: 1, ... }
this.registrationOrder = {}; this.registrationOrder = {};
this.nextNumber = 0; this.nextNumber = 0;
this.extSpecificTypes = [];
}; };
/* Inheritance */ /* Inheritance */
@ -82,9 +83,6 @@ ve.dm.ModelRegistry.prototype.register = function ( constructor ) {
ve.dm.annotationFactory.register( name, constructor ); ve.dm.annotationFactory.register( name, constructor );
} else if ( constructor.prototype instanceof ve.dm.Node ) { } else if ( constructor.prototype instanceof ve.dm.Node ) {
ve.dm.nodeFactory.register( name, constructor ); 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 { } else {
throw new Error( 'Models must be subclasses of ve.dm.Annotation or ve.dm.Node' ); 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++; 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 * 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 * @returns {string|null} Model type, or null if none found
*/ */
ve.dm.ModelRegistry.prototype.matchElement = function ( element ) { 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(), tag = element.nodeName.toLowerCase(),
typeAttr = element.getAttribute( 'typeof' ) || element.getAttribute( 'rel' ), typeAttr = element.getAttribute( 'typeof' ) || element.getAttribute( 'rel' ),
reg = this; reg = this;
@ -180,9 +210,13 @@ ve.dm.ModelRegistry.prototype.matchElement = function ( element ) {
} }
types = typeAttr ? typeAttr.split( ' ' ) : []; 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 ) { if ( types.length ) {
// func+tag+type match // func+tag+type match
winner = matchWithFunc( types, tag ); winner = matchWithFunc( matchTypes, tag );
if ( winner !== null ) { if ( winner !== null ) {
return winner; return winner;
} }
@ -190,42 +224,45 @@ ve.dm.ModelRegistry.prototype.matchElement = function ( element ) {
// func+type match // func+type match
// Only look at rules with no tag specified; if a rule does specify a tag, we've // 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 // either already processed it above, or the tag doesn't match
winner = matchWithFunc( types, '' ); winner = matchWithFunc( matchTypes, '' );
if ( winner !== null ) { if ( winner !== null ) {
return winner; return winner;
} }
} }
// func+tag match // Do not check for type-less matches if the element has extension-specific types
matches = ve.getProp( this.modelsByTag, 1, tag ) || []; if ( elementExtSpecificTypes.length === 0 ) {
// No need to sort because individual arrays in modelsByTag are already sorted // func+tag match
// correctly matches = ve.getProp( this.modelsByTag, 1, tag ) || [];
for ( i = 0; i < matches.length; i++ ) { // No need to sort because individual arrays in modelsByTag are already sorted
name = matches[i]; // correctly
model = this.registry[name]; for ( i = 0; i < matches.length; i++ ) {
// Only process this one if it doesn't specify types name = matches[i];
// If it does specify types, then we've either already processed it in the model = this.registry[name];
// func+tag+type step above, or its type rule doesn't match // Only process this one if it doesn't specify types
if ( model.static.matchRdfaTypes === null && model.static.matchFunction( element ) ) { // If it does specify types, then we've either already processed it in the
return matches[i]; // 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 // func only
// We only need to get the [''][''] array because the other arrays were either // 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 // already processed during the steps above, or have a type or tag rule that doesn't
// match this element. // match this element.
// No need to sort because individual arrays in modelsByTypeAndTag are already sorted // No need to sort because individual arrays in modelsByTypeAndTag are already sorted
// correctly // correctly
matches = ve.getProp( this.modelsByTypeAndTag, 1, '', '' ) || []; matches = ve.getProp( this.modelsByTypeAndTag, 1, '', '' ) || [];
for ( i = 0; i < matches.length; i++ ) { for ( i = 0; i < matches.length; i++ ) {
if ( this.registry[matches[i]].static.matchFunction( element ) ) { if ( this.registry[matches[i]].static.matchFunction( element ) ) {
return matches[i]; return matches[i];
}
} }
} }
// tag+type // tag+type
winner = matchWithoutFunc( types, tag ); winner = matchWithoutFunc( matchTypes, tag );
if ( winner !== null ) { if ( winner !== null ) {
return winner; return winner;
} }
@ -233,14 +270,20 @@ ve.dm.ModelRegistry.prototype.matchElement = function ( element ) {
// type only // type only
// Only look at rules with no tag specified; if a rule does specify a tag, we've // 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 // either already processed it above, or the tag doesn't match
winner = matchWithoutFunc( types, '' ); winner = matchWithoutFunc( matchTypes, '' );
if ( winner !== null ) { if ( winner !== null ) {
return winner; 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 // tag only
matches = ve.getProp( this.modelsByTag, 0, tag ) || []; 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 // already sorted correctly
for ( i = 0; i < matches.length; i++ ) { for ( i = 0; i < matches.length; i++ ) {
name = matches[i]; name = matches[i];

View file

@ -72,9 +72,20 @@ ve.dm.StubSingleTagAndTypeAndFuncAnnotation.static.matchTagNames = ['a'];
ve.dm.StubSingleTagAndTypeAndFuncAnnotation.static.matchRdfaTypes = ['mw:foo']; ve.dm.StubSingleTagAndTypeAndFuncAnnotation.static.matchRdfaTypes = ['mw:foo'];
ve.dm.StubSingleTagAndTypeAndFuncAnnotation.static.matchFunction = checkForPickMe; 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 */ /* Tests */
QUnit.test( 'matchElement', 9, function ( assert ) { QUnit.test( 'matchElement', 16, function ( assert ) {
var registry = new ve.dm.ModelRegistry(), element; var registry = new ve.dm.ModelRegistry(), element;
element = document.createElement( 'a' ); element = document.createElement( 'a' );
assert.deepEqual( registry.matchElement( element ), null, 'matchElement() returns null if registry empty' ); 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.StubSingleTagAndFuncAnnotation );
registry.register( ve.dm.StubSingleTypeAndFuncAnnotation ); registry.register( ve.dm.StubSingleTypeAndFuncAnnotation );
registry.register( ve.dm.StubSingleTagAndTypeAndFuncAnnotation ); registry.register( ve.dm.StubSingleTagAndTypeAndFuncAnnotation );
registry.register( ve.dm.StubBarNode );
element = document.createElement( 'b' ); element = document.createElement( 'b' );
assert.deepEqual( registry.matchElement( element ), 'stubnothingset', 'nothingset matches anything' ); 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' ); assert.deepEqual( registry.matchElement( element ), 'stubfunc', 'func-only match' );
element.setAttribute( 'rel', 'mw:foo' ); element.setAttribute( 'rel', 'mw:foo' );
assert.deepEqual( registry.matchElement( element ), 'stubsingletypeandfunc', 'type and func match' ); 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' );
} ); } );