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, ... }
|
// { 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];
|
||||||
|
|
|
@ -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' );
|
||||||
} );
|
} );
|
||||||
|
|
Loading…
Reference in a new issue