From d73d6e9bf015d96fcd679736712cbc2fcd011af8 Mon Sep 17 00:00:00 2001 From: Catrope Date: Tue, 22 Jan 2013 14:50:35 -0800 Subject: [PATCH] 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 would either match a rule specifically for mw:foobar, if one exists, or no rule at all; even the rule for would not match. The consequence of this is that elements with unrecognized mw:-prefixed RDFa types are alienated. Change-Id: Ia8ab1fe5dffb9f813689324372a168e8e4a3e0bc --- modules/ve/dm/ve.dm.ModelRegistry.js | 107 ++++++++++++------ .../ve/test/dm/ve.dm.ModelRegistry.test.js | 32 +++++- 2 files changed, 106 insertions(+), 33 deletions(-) diff --git a/modules/ve/dm/ve.dm.ModelRegistry.js b/modules/ve/dm/ve.dm.ModelRegistry.js index 968b50ed20..13e249a266 100644 --- a/modules/ve/dm/ve.dm.ModelRegistry.js +++ b/modules/ve/dm/ve.dm.ModelRegistry.js @@ -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 + * 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]; diff --git a/modules/ve/test/dm/ve.dm.ModelRegistry.test.js b/modules/ve/test/dm/ve.dm.ModelRegistry.test.js index 42a3c0dcaa..ed09edf196 100644 --- a/modules/ve/test/dm/ve.dm.ModelRegistry.test.js +++ b/modules/ve/test/dm/ve.dm.ModelRegistry.test.js @@ -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' ); } );