%PDF- %PDF-
Direktori : /www/varak.net/wiki.varak.net/extensions/VisualEditor/lib/ve/src/dm/ |
Current File : /www/varak.net/wiki.varak.net/extensions/VisualEditor/lib/ve/src/dm/ve.dm.ModelRegistry.js |
/*! * VisualEditor ModelRegistry class. * * @copyright 2011-2016 VisualEditor Team and others; see http://ve.mit-license.org */ ( function ( ve ) { /** * Registry for models. * * To register a new model type, call #register. * * @extends OO.Registry * @constructor */ ve.dm.ModelRegistry = function VeDmModelRegistry() { // Parent constructor OO.Registry.call( this ); // Map of func presence and tag names to model names // [ { tagName: [modelNamesWithoutFunc] }, { tagName: [modelNamesWithFunc] } ] this.modelsByTag = [ {}, {} ]; // Map of func presence and rdfaTypes to model names; only rdfaTypes specified as strings are in here // { matchFunctionPresence: { rdfaType: { tagName: [modelNames] } } } // [ { rdfaType: { tagName: [modelNamesWithoutFunc] } }, { rdfaType: { tagName: [modelNamesWithFunc] } ] this.modelsByTypeAndTag = []; // Map of func presence to array of model names with rdfaType regexps // [ [modelNamesWithoutFunc], [modelNamesWithFunc] ] this.modelsWithTypeRegExps = [ [], [] ]; // Map tracking registration order // { nameA: 0, nameB: 1, ... } this.registrationOrder = {}; this.nextNumber = 0; this.extSpecificTypes = []; }; /* Inheritance */ OO.inheritClass( ve.dm.ModelRegistry, OO.Registry ); /* 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 one or more keys and a value. * * Specifically: * * - `addType( obj, keyA, value )` does `obj[keyA].unshift( value );` * - `addType( obj, keyA, keyB, value )` does `obj[keyA][keyB].unshift( value )`; * - etc. * * @private * @param {Object} obj Object the array resides in * @param {...string} keys * @param {Mixed} value */ function addType( obj ) { var i, len, keys = Array.prototype.slice.call( arguments, 1, -1 ), value = arguments[ arguments.length - 1 ], o = obj; for ( i = 0, len = keys.length - 1; i < len; i++ ) { if ( o[ keys[ i ] ] === undefined ) { o[ keys[ i ] ] = {}; } o = o[ keys[ i ] ]; } o[ keys[ i ] ] = o[ keys[ i ] ] || []; o[ keys[ i ] ].unshift( value ); } /** * Helper function for unregister(). * * Same arguments as addType, except removes the type from the list. * * @private * @param {Object} obj Object the array resides in * @param {...string} keys * @param {Mixed} value to remove */ function removeType( obj ) { var index, keys = Array.prototype.slice.call( arguments, 1, -1 ), value = arguments[ arguments.length - 1 ], arr = ve.getProp.apply( obj, [ obj ].concat( keys ) ); if ( arr ) { index = arr.indexOf( value ); if ( index !== -1 ) { arr.splice( index, 1 ); } // TODO: Prune empty array and empty containing objects } } /* Public methods */ /** * Register a model type. * * @param {ve.dm.Model} constructor Subclass of ve.dm.Model * @throws Model names must be strings and must not be empty * @throws Models must be subclasses of ve.dm.Model * @throws No factory associated with this ve.dm.Model subclass */ ve.dm.ModelRegistry.prototype.register = function ( constructor ) { var i, j, tags, types, name = constructor.static && constructor.static.name; if ( typeof name !== 'string' || name === '' ) { throw new Error( 'Model names must be strings and must not be empty' ); } if ( !( constructor.prototype instanceof ve.dm.Model ) ) { throw new Error( 'Models must be subclasses of ve.dm.Model' ); } // Register the model with the right factory if ( constructor.prototype instanceof ve.dm.Annotation ) { ve.dm.annotationFactory.register( constructor ); } else if ( constructor.prototype instanceof ve.dm.Node ) { ve.dm.nodeFactory.register( constructor ); } else if ( constructor.prototype instanceof ve.dm.MetaItem ) { ve.dm.metaItemFactory.register( constructor ); } else { throw new Error( 'No factory associated with this ve.dm.Model subclass' ); } // Parent method ve.dm.ModelRegistry.super.prototype.register.call( this, name, constructor ); tags = constructor.static.matchTagNames === null ? [ '' ] : constructor.static.matchTagNames; types = constructor.static.getMatchRdfaTypes() === null ? [ '' ] : constructor.static.getMatchRdfaTypes(); for ( i = 0; i < tags.length; i++ ) { // +!!foo is a shorter equivalent of Number( Boolean( foo ) ) or foo ? 1 : 0 addType( this.modelsByTag, +!!constructor.static.matchFunction, tags[ i ], name ); } for ( i = 0; i < types.length; i++ ) { if ( types[ i ] instanceof RegExp ) { // TODO: Guard against running this again during subsequent // iterations of the for loop addType( this.modelsWithTypeRegExps, +!!constructor.static.matchFunction, name ); } else { for ( j = 0; j < tags.length; j++ ) { addType( this.modelsByTypeAndTag, +!!constructor.static.matchFunction, types[ i ], tags[ j ], name ); } } } this.registrationOrder[ name ] = this.nextNumber++; }; /** * Unregister a model type. * * @param {ve.dm.Model} constructor Subclass of ve.dm.Model * @throws Model names must be strings and must not be empty * @throws Models must be subclasses of ve.dm.Model * @throws No factory associated with this ve.dm.Model subclass */ ve.dm.ModelRegistry.prototype.unregister = function ( constructor ) { var i, j, tags, types, name = constructor.static && constructor.static.name; if ( typeof name !== 'string' || name === '' ) { throw new Error( 'Model names must be strings and must not be empty' ); } if ( !( constructor.prototype instanceof ve.dm.Model ) ) { throw new Error( 'Models must be subclasses of ve.dm.Model' ); } // Unregister the model from the right factory if ( constructor.prototype instanceof ve.dm.Annotation ) { ve.dm.annotationFactory.unregister( constructor ); } else if ( constructor.prototype instanceof ve.dm.Node ) { ve.dm.nodeFactory.unregister( constructor ); } else if ( constructor.prototype instanceof ve.dm.MetaItem ) { ve.dm.metaItemFactory.unregister( constructor ); } else { throw new Error( 'No factory associated with this ve.dm.Model subclass' ); } // Parent method ve.dm.ModelRegistry.super.prototype.unregister.call( this, name ); tags = constructor.static.matchTagNames === null ? [ '' ] : constructor.static.matchTagNames; types = constructor.static.getMatchRdfaTypes() === null ? [ '' ] : constructor.static.getMatchRdfaTypes(); for ( i = 0; i < tags.length; i++ ) { // +!!foo is a shorter equivalent of Number( Boolean( foo ) ) or foo ? 1 : 0 removeType( this.modelsByTag, +!!constructor.static.matchFunction, tags[ i ], name ); } for ( i = 0; i < types.length; i++ ) { if ( types[ i ] instanceof RegExp ) { // TODO: Guard against running this again during subsequent // iterations of the for loop removeType( this.modelsWithTypeRegExps, +!!constructor.static.matchFunction, name ); } else { for ( j = 0; j < tags.length; j++ ) { removeType( this.modelsByTypeAndTag, +!!constructor.static.matchFunction, types[ i ], tags[ j ], name ); } } } delete this.registrationOrder[ name ]; }; /** * Determine which model best matches the given node * * Model matching works as follows: * * Get all models 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 model whose test function does not return false, wins. * * @param {Node} node Node to match (usually an HTMLElement but can also be a Comment node) * @param {boolean} [forceAboutGrouping] If true, only match models with about grouping enabled * @param {string[]} [excludeTypes] Model names to exclude when matching * @return {string|null} Model type, or null if none found */ ve.dm.ModelRegistry.prototype.matchElement = function ( node, forceAboutGrouping, excludeTypes ) { var i, name, model, matches, winner, types, tag = node.nodeName.toLowerCase(), reg = this; function byRegistrationOrderDesc( a, b ) { return reg.registrationOrder[ b ] - reg.registrationOrder[ a ]; } function matchTypeRegExps( type, tag, withFunc ) { var i, j, types, matches = [], models = reg.modelsWithTypeRegExps[ +withFunc ]; for ( i = 0; i < models.length; i++ ) { if ( excludeTypes && excludeTypes.indexOf( models[ i ] ) !== -1 ) { continue; } types = reg.registry[ models[ i ] ].static.getMatchRdfaTypes(); for ( j = 0; j < types.length; j++ ) { if ( types[ j ] instanceof RegExp && type.match( types[ j ] ) && ( ( tag === '' && reg.registry[ models[ i ] ].static.matchTagNames === null ) || ( reg.registry[ models[ i ] ].static.matchTagNames || [] ).indexOf( tag ) !== -1 ) ) { matches.push( models[ i ] ); } } } return matches; } function allTypesAllowed( types, name ) { var i, j, typeAllowed, model = reg.lookup( name ), allowedTypes = model.static.getAllowedRdfaTypes(), matchTypes = model.static.getMatchRdfaTypes(); // All types allowed if ( allowedTypes === null || matchTypes === null ) { return true; } allowedTypes = allowedTypes.concat( matchTypes ); function checkType( rule, type ) { return rule instanceof RegExp ? !!type.match( rule ) : rule === type; } for ( i = 0; i < types.length; i++ ) { typeAllowed = false; for ( j = 0; j < allowedTypes.length; j++ ) { if ( checkType( allowedTypes[ j ], types[ i ] ) ) { typeAllowed = true; break; } } if ( !typeAllowed ) { return false; } } return true; } function matchWithFunc( types, tag ) { var i, queue = [], queue2 = []; for ( i = 0; i < types.length; i++ ) { // Queue string matches and regexp matches separately queue = queue.concat( ve.getProp( reg.modelsByTypeAndTag, 1, types[ i ], tag ) || [] ); if ( excludeTypes ) { queue = OO.simpleArrayDifference( queue, excludeTypes ); } queue2 = queue2.concat( matchTypeRegExps( types[ i ], tag, true ) ); } // Filter out matches which contain types which aren't allowed queue = queue.filter( function ( name ) { return allTypesAllowed( types, name ); } ); queue2 = queue2.filter( function ( name ) { return allTypesAllowed( types, name ); } ); if ( forceAboutGrouping ) { // Filter out matches that don't support about grouping queue = queue.filter( function ( name ) { return reg.registry[ name ].static.enableAboutGrouping; } ); queue2 = queue2.filter( function ( name ) { return reg.registry[ name ].static.enableAboutGrouping; } ); } // Try string matches first, then regexp matches queue.sort( byRegistrationOrderDesc ); queue2.sort( byRegistrationOrderDesc ); queue = queue.concat( queue2 ); for ( i = 0; i < queue.length; i++ ) { if ( reg.registry[ queue[ i ] ].static.matchFunction( node ) ) { return queue[ i ]; } } return null; } function matchWithoutFunc( types, tag ) { var i, queue = [], queue2 = [], winningName = null; for ( i = 0; i < types.length; i++ ) { // Queue string and regexp matches separately queue = queue.concat( ve.getProp( reg.modelsByTypeAndTag, 0, types[ i ], tag ) || [] ); if ( excludeTypes ) { queue = OO.simpleArrayDifference( queue, excludeTypes ); } queue2 = queue2.concat( matchTypeRegExps( types[ i ], tag, false ) ); } // Filter out matches which contain types which aren't allowed queue = queue.filter( function ( name ) { return allTypesAllowed( types, name ); } ); queue2 = queue2.filter( function ( name ) { return allTypesAllowed( types, name ); } ); if ( forceAboutGrouping ) { // Filter out matches that don't support about grouping queue = queue.filter( function ( name ) { return reg.registry[ name ].static.enableAboutGrouping; } ); queue2 = queue2.filter( function ( name ) { return reg.registry[ name ].static.enableAboutGrouping; } ); } // Only try regexp matches if there are no string matches queue = queue.length > 0 ? queue : queue2; // Find most recently registered for ( i = 0; i < queue.length; i++ ) { if ( winningName === null || reg.registrationOrder[ winningName ] < reg.registrationOrder[ queue[ i ] ] ) { winningName = queue[ i ]; } } return winningName; } types = []; if ( node.getAttribute ) { if ( node.getAttribute( 'rel' ) ) { types = types.concat( node.getAttribute( 'rel' ).split( ' ' ) ); } if ( node.getAttribute( 'typeof' ) ) { types = types.concat( node.getAttribute( 'typeof' ).split( ' ' ) ); } if ( node.getAttribute( 'property' ) ) { types = types.concat( node.getAttribute( 'property' ).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.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.getMatchRdfaTypes() === null && model.static.matchFunction( node ) ) { 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 node. // 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( node ) ) { 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.modelsByTag, 0, tag ) || []; // 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 ]; 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 // tag+type step above, or its type rule doesn't match if ( model.static.getMatchRdfaTypes() === 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.modelsByTypeAndTag, 0, '', '' ) || []; if ( matches.length > 0 ) { return matches[ 0 ]; } // We didn't find anything, give up return null; }; /** * Tests whether a node will be modelled as an annotation * * @param {Node} node The node * @return {boolean} Whether the element will be modelled as an annotation */ ve.dm.ModelRegistry.prototype.isAnnotation = function ( node ) { var modelClass = this.lookup( this.matchElement( node ) ); return ( modelClass && modelClass.prototype ) instanceof ve.dm.Annotation; }; /* Initialization */ ve.dm.modelRegistry = new ve.dm.ModelRegistry(); } )( ve );