... format. This is the fall back case and it might work reasonably well * for very simple objects. However, it is often the case that more powerful handlers are needed * to accommodate for things like browser quirks. * * Therefore the embed module can be extended by other handlers (i.e. themes) that are designed to * process particular types of object. Objects are defined by their mime type * (en.wikipedia.org/wiki/Internet_media_type) since this is a standard way of defining what the * object is referring to. When other handlers register with the embed module then the settings * page will update to present lists of handlers that are available for a given type of object. * The preferred handler can then be chosen and all rendering of that type of object will be * passed to that handler, or theme. * * It is is possible to "chain" handlers together. For example, a handler for audio/mp3 might * return a flash audio player. Behind the scenes the correct course of action would be for * the player module to construct a new object element describing the flash player. It should * then call drupal_render() to render the new object element. Why is that necessary? Flash content * is in itself a type of object, and there may be multiple handlers presenting for rendering * application/x-shockwave-flash in to markup. By calling drupal_render() the audio player module * can output its content using any available flash rendering theme - it is not necessary for the * player module to provide its own flash rendering code. * * The module is also designed to work in a similar way to CCK for the case of multiple objects. * If an object element contains an array of objects then the embed module will try to determine * the handler. The module will then query the handler to discover it understands object arrays. If * the handler reports that it does then the embed module will pass the array to the handler. So, * for example, a collection of audio/mp3 content might be turned in to a playlist in a single * media player. If the handler does not understand multiple objects then the embed module will * render each object individually as a discrete piece of markup. * * The intention of the embed module is to create a consistent way for modules to handle object * content. At the momoment there are various resources, varying degrees of overlap, * and if a user wants to change from one embedding method to another then it might * not be straightforward. By treating objects as an element the rendering is * handled centrally through the theme system, allowing all modules to access all the handlers * that are available. * */ /** * The theme cannot handle multiple objects and items should be rendered individually. */ define('EMBED_HANDLE_CORE', 0x0001); /** * The theme can handle multiple objects and a collection of objects can be passed. */ define('EMBED_HANDLE_MODULE', 0x0002); /** * The embedding cache should be reset when embed_get_info() is called. */ define('EMBED_RESET_CACHE', TRUE); /** * The path to embedding resource files, such as JavaScript. */ define('EMBED_RESOURCE_PATH', 'sites/all/modules/embed_resources/'); /** * Theme a generic object. * * @param $element * The element to theme. * @return * A string of markup. */ function theme_object($element) { // Generic object embedding $element['#parameters'] = embed_build_parameters_string($element['#parameters']); // Create mark up for the object $return[] = ''; $return[] = $element['#parameters']; $return[] = $element['#value']; $return[] = ''; // Format the result nicely $return = embed_format_output($return); // Return the result return $return; } /** * Turn an array of parameters in to a string. * * @param $parameters * An array of parameters as parameter => value. * @return * A string ready for inclusion in page markup, or the original value if it wasn't an array. */ function embed_build_parameters_string($parameters) { // Only try to process $element['#parameters'] if it is an array if (is_array($parameters)) { // Turn each value of $element['#parameters'] in to a string ready for inclusion foreach($parameters as $parameter => $value) { // Build a parameters string TODO : Do we need to check_plain($parameter)? $parameters[$parameter] = ''; } // Collapse the $parameters array in to a string ready for inclusion $parameters = implode("\n", $parameters); } // Return the result return $parameters; } /** * Ensure that the provided id is unique, or if no id is set create one. * * JavaScript embedding methods typically use the id of a container element to work out where to put the * swf content. Therefore it is necessary to ensure that each id is unique for the page in question. * * If no id is set when the function is called then it will generate one in the format 'embed-nnnn' where * nnnn is the time, as given by the time() function. This means that ids will be unique in the event that * the content is cached. * * If an id is provided when the function is called then that id will be checked against those already used. * If the id was used before then it will be appended with a hyphen and a number to make it unique. This is * also applied to generated ids since multiple flash content can result in the same time being used more * than once. * * The function also cleans up the id by replacing underscores or spaces with hyphens. * * The (potentially modified) id is then returned. Note that this function will not 100% guarantee the uniqueness * of an id. If a specific id is set on content generated by a filter then the function will only verify that * the id is unique at the time of submission. If another node is submitted later with the same id then this * will be accepted. If the two items of cached content are then placed on the same page there will be two * occurrences of the id and the embedding method may fail. * * In general then unless there is a need to use a specific id, perhaps to allow some other JavaScript to * target the content, then it is probably best to let the API generate its own ids since these are certain to * be unique. * * @param $id * (optional) The current id. * @param $reset * (optional) If TRUE then flush the cache of ids already seen. * @return * An id that is cleaned and unique. */ function embed_clean_id($id = NULL, $reset = FALSE) { // Initialize a static variable to hold the ids that have been seen so far $seen_ids = &drupal_static(__FUNCTION__, array()); // If a cache flush was requested then do it if ($reset) { $seen_ids = array(); return; } // If no id was set then generate a default one using the system time if (empty($id)) { $id = 'embed-' . time(); } else { $id = str_replace(array('][', '_', ' '), '-', $id); } // TODO : I think Wijering complains if the object id doesn't start with a letter? // If we already saw this id, append a number to make it unique if (isset($seen_ids[$id])) { $id = $id .'-'. $seen_ids[$id]++; } else { $seen_ids[$id] = 1; } // Return the prepared id return $id; } /** * Implementation of hook_elements(). */ function embed_elements() { // TODO : what is the minimum definition for an object? $type['object'] = array( '#attributes' => array( 'data' => '', 'type' => '', 'id' => '', ), '#parameters' => array(), '#pre_render' => array('embed_pre_render'), '#module_data' => array( 'embed' => array(), ), '#theme' => '', '#value' => '', ); // Return the type return $type; } /** * Get information from modules that have themes that can be made available to the embed module. * * This function calls hook_embed_info() in all modules so that they can report back information * about the themes that they are offering to the module for handling objects. * * Modules may optionally report additional data, such as the name of the supporting JavaScript library, the * download path to the library, and whether the theme is a private method and not for display on the * settings page. * * If information for a specific theme is required then a theme name can be passed as the first * parameter, in which case only data for that theme will be returned. * * @see hook_embed_info() * * @param $theme * (optional) The name of a specific theme to return info for. * @param $reset * (optional) If TRUE flush the embed info cache. * @return * An array of information about available embedding themes, or data for a specific theme. */ function embed_get_info($theme = '', $reset = FALSE) { // Initialize a static variable static $info; // Try to retrieve date from the cache if we can if (!isset($info) || $reset) { if (!$reset && ($cache = cache_get('embed:theme_info')) && !empty($cache->data)) { $info = $cache->data; } else { $info = module_invoke_all('embed_info'); cache_set('embed:theme_info', $info, 'cache', CACHE_PERMANENT); } } // Return info a specific theme if requested; empty array for theme with no info; otherwise all info if ($theme && isset($info[$theme])) { return $info[$theme]; } elseif ($theme) { return array(); } else { return $info; } } /** * Implementation of hook_help(). */ function embed_help($path, $arg) { switch ($path) { case 'admin/settings/embed/handlers': return '

'.t('The embed module enables support for object elements. By installing additional components the module can then use a variety of methods to display the elements on a page. Default handlers can be set for each type of object for which a specific handler is available. If no custom handlers are enabled then the module will fall back to rendering the object using simple <object>...</object> format. The handler can be changed at any time so it is possible to try different embedding methods very easily.').'

'. t('Note that some JavaScript methods require the use of third party script resources. These must be downloaded and added to the appropriate locations.').'

'; } } /** * Turn an array of markup lines in to a string wrapped in header and footer comments. * * @param $content * An array where each entry is a line of mark up ready to be output. * @return * A string of markup that has been formatted and wrapped in comments. */ function embed_format_output($content) { // A simple comment wrapper $return[] = ''; $return[] = ''; $return[] = ''; // Add the content $return = array_merge($return, $content); // Close wrapper $return[] = ''; $return[] = ''; $return[] = ''; // Collapse in to a string with with line breaks $return = implode("\n", $return); // Return the result return $return; } /** * Determine the mime type of an arbitrary array of object element children. * * If all the mime types are the same then the mime type of the parent element * is set to that of its children. This so that a handler knows it is receiving * multiple elements of one type without having to interrogate the array itself. * * If the mime types vary then the parent type is set to multipart/mixed. A receiving * handler now knows that it will need to investigate the array to find out what it * consists of. * * @param $elements * The element (with children) to interrogate. * @return * A mimetype indicating the contents of the child elements. */ function embed_get_multiple_object_type($elements) { // Iterate through all the child elements to determine their type and use the array to track the results foreach (element_children($elements) as $key) { $types[$elements[$key]['#attributes']['type']] = $elements[$key]['#attributes']['type']; } // If there is only one element in $types then there is only one type of child if (count($types) == 1) { return array_pop($types); } // If we get here then children are mixed type return 'multipart/mixed'; } /** * Pre-render an object element ready for output. * * The first purpose of this function is to assign and/or verify that the id for the * object is unique. Secondly, the routine is used to determine how arrays of objects * should be handled. * * First, the mime type of the parent is assigned, based on the contents of the children. * If all children are of the same type then the parent will be assigned that type. If the * children are of mixed type the parent will be assigned as multipart/mixed. * * The function then determines what theme would be assigned to that type, but it does not * actually assign it at this stage as that will upset the render function. However, based * on the handler capabilities the function may flatten the array of objects in to separate * elements, if the handler cannot process a collection of objects. * * This function does NOT assign themes to an element. Assignment of id and conversion of * arrays is done here since a specific theme may be assigned, in which case theme_object() * is never called. It also means that theme_object() is ONLY concerned with the rendering * of an object. * * @param $element * An element to be pre-rendered. * @return * A modified element. */ function embed_pre_render($element) { // TODO : Allow a module to manipulate the object directly before rendering? // Is this necessary? I am thinking, for example, that an ID3 module might // insert data before any further transformation is done. drupal_alter('object', $element); // First and easiest case - there no children and therefore this is a final element if (!element_children($element)) { // Assign an appropriate theme if one wasn't explicitly set $element['#theme'] = embed_get_theme($element); // Attach an id, or check id is unique $element = embed_prepare_id($element); // Return modified element return $element; } // There are children - work out what the parent mimetype is (mixed, or single type) $element['#attributes']['type'] = embed_get_multiple_object_type($element); // Assign an appropriate theme if one wasn't explicitly set $element['#theme'] = embed_get_theme($element); // Retrieve information about this theme $theme = embed_get_info($element['#theme']); // Add a default of EMBED_HANDLE_CORE in case the theme didn't specify $theme += array( 'multiple values' => EMBED_HANDLE_CORE, ); // If theme can handle multiple objects then assign an id to the parent and render the parent if ($theme['multiple values'] == EMBED_HANDLE_MODULE) { // Attach an id, or check id is unique $element = embed_prepare_id($element); // Return return $element; } // If here then the theme is not multiple object capable so transform children to discrete elements foreach (element_children($element) as $key) { // Set each child element to an object $element[$key]['#type'] = 'object'; // TODO : If the parent had a specific theme then cascade it to the children? } // The parent should no longer be rendered so unset #type unset($element['#type']); // The parent element will also be rendered if a theme is set, so unset that if (isset($element['#theme'])) { unset($element['#theme']); } // Return modified element return $element; } /** * Determine the theme for rendering a specific object type. * * If no specific theme is set on the element then the API will retrieve the setting from the embed settings. * * If no specific handler is stored in the settings it will return object which is a generic handler that will try to embed any object. * * @param $element * The element to determine a theme for. * @return * The name of a theme for rendering the element. */ function embed_get_theme($element) { // TODO: Check the requested theme actually exists so that if a handler is disabled // it will fall back to generic embedding // If a specific theme is already set then return that if($element['#theme']) { return $element['#theme']; } // Find out what the current assigned handler is for this type, or assign object if none $theme = variable_get('embed_'.$element['#attributes']['type'], 'object'); // Return the assigned theme return $theme; } /** * Reorder data returned by hook_embed_info() so it is keyed on type, not theme. * * This function is used by the settings pages, and also by the menu callback, since * this presents data organised by mime type. For normal use within the module this * information is keyed on the theme name as this is easier to interrogate. * * @param $themes * An array of theme data from a call to hook_embed_info(). * @return * An array restructured to be ordered by type, not by theme. */ function embed_remap_info($themes) { // Iterate through each theme foreach ($themes as $theme => $data) { // Iterate through each type that the theme can handle and create entries in the new array foreach ($data['types'] as $type) { $new_themes[$type][$theme] = $data; } } // Return the restructured array return $new_themes; } /** * Prepare a unique id for the object prior to rendering it. * * Assign an id to the element if one is not set, or verify that the supplied id is unique in this page building session and amend it if necessary. * * When the id has been cleaned a flag is set on the element (#id_cleaned) to * show that it has been processed. This is necessary to prevent the id * 'incrementing' for content that is process through multiple object types * before final render. * * @param $element * The element to prepare for return. * @return * A modified element. */ function embed_prepare_id($element) { // Ensure an id is set if one wasn't provided $element['#attributes'] += array( 'id' => '', ); // See if we need to check the id on this element is unique if (!isset($element['#id_cleaned'])) { // Assign an id by default / ensure id is unique $element['#attributes']['id'] = embed_clean_id($element['#attributes']['id']); // Set flag to show that id has been cleaned up $element['#id_cleaned'] = TRUE; } // Return the modified element return $element; } /** * Cancel #prefix, #suffix, #pre_render, #post_render since these should not be called again. * * @param $element * The element on which these properties should be cancelled. * @return * A modified element, with relevant properties cancelled. */ function embed_cancel_repeated_properties($element) { // Unset #prefix and #suffix since we already called drupal_render() which will write these unset($element['#prefix']); unset($element['#suffix']); // Unset #pre_render and #spost_render since we already called drupal_render() which will do this unset($element['#pre_render']); unset($element['#post_render']); // Return the modified element return $element; }