' . t('About') . ''; $output .= '

' . t('The Search module provides the ability to index and search for content by exact keywords, and for users by username or e-mail. For more information, see the online handbook entry for Search module.', array('@search-module' => 'http://drupal.org/handbook/modules/search/', '@search' => url('search'))) . '

'; $output .= '

' . t('Uses') . '

'; $output .= '
'; $output .= '
' . t('Searching content and users') . '
'; $output .= '
' . t('Users with Use search permission can use the search block and Search page. Users with the View published content permission can search for content containing exact keywords. Users with the View user profiles permission can search for users containing the keyword anywhere in the user name, and users with the Administer users permission can search for users by email address. Additionally, users with Use advanced search permission can find content using more complex search methods and filtering by choosing the Advanced search option on the Search page.', array('@search' => url('search'))) . '
'; $output .= '
' . t('Indexing content with cron') . '
'; $output .= '
' . t('To provide keyword searching, the search engine maintains an index of words found in the content. To build and maintain this index, a correctly configured cron maintenance task is required. Users with Administer search permission can further configure the cron settings on the Search settings page.', array('@cron' => 'http://drupal.org/cron', '@searchsettings' => url('admin/config/search/settings'))) . '
'; $output .= '
' . t('Configuring search settings') . '
'; $output .= '
' . t('Indexing behavior can be adjusted using the Search settings page. Users with Administer search permission can control settings such as the Number of items to index per cron run, Indexing settings (word length), Active search modules, and Content ranking, which lets you adjust the priority in which indexed content is returned in results.', array('@searchsettings' => url('admin/config/search/settings'))) . '
'; $output .= '
' . t('Search block') . '
'; $output .= '
' . t('The Search module includes a default Search form block, which can be enabled and configured on the Blocks administration page. The block is available to users with the Search content permission.', array('@blocks' => url('admin/structure/block'))) . '
'; $output .= '
' . t('Extending Search module') . '
'; $output .= '
' . t('By default, the Search module only supports exact keyword matching in content searches. You can modify this behavior by installing a language-specific stemming module for your language (such as Porter Stemmer for American English), which allows words such as walk, walking, and walked to be matched in the Search module. Another approach is to use a third-party search technology with stemming or partial word matching features built in, such as Apache Solr or Sphinx. These and other search-related contributed modules can be downloaded by visiting Drupal.org.', array('@contrib-search' => 'http://drupal.org/project/modules?filters=tid%3A105')) . '
'; $output .= '
'; return $output; case 'admin/config/search/settings': return '

' . t('The search engine maintains an index of words found in your site\'s content. To build and maintain this index, a correctly configured cron maintenance task is required. Indexing behavior can be adjusted using the settings below.', array('@cron' => url('admin/reports/status'))) . '

'; case 'search#noresults': return t(''); } } /** * Implements hook_theme(). */ function search_theme() { return array( 'search_block_form' => array( 'render element' => 'form', 'template' => 'search-block-form', ), 'search_result' => array( 'variables' => array('result' => NULL, 'type' => NULL), 'file' => 'search.pages.inc', 'template' => 'search-result', ), 'search_results' => array( 'variables' => array('results' => NULL, 'type' => NULL), 'file' => 'search.pages.inc', 'template' => 'search-results', ), 'search_results_listing' => array( 'variables' => array('title' => NULL, 'content' => NULL), ), ); } /** * Implements hook_permission(). */ function search_permission() { return array( 'administer search' => array( 'title' => t('Administer search'), ), 'search content' => array( 'title' => t('Use search'), ), 'use advanced search' => array( 'title' => t('Use advanced search'), ), ); } /** * Implements hook_block_info(). */ function search_block_info() { $blocks['form']['info'] = t('Search form'); // Not worth caching. $blocks['form']['cache'] = DRUPAL_NO_CACHE; return $blocks; } /** * Implements hook_block_view(). */ function search_block_view($delta = '') { if (user_access('search content')) { $block['content'] = drupal_get_form('search_block_form'); return $block; } } /** * Implements hook_menu(). */ function search_menu() { $items['search'] = array( 'title' => 'Search', 'page callback' => 'search_view', 'access arguments' => array('search content'), 'type' => MENU_SUGGESTED_ITEM, 'file' => 'search.pages.inc', ); $items['admin/config/search/settings'] = array( 'title' => 'Search settings', 'description' => 'Configure relevance settings for search and other indexing options.', 'page callback' => 'drupal_get_form', 'page arguments' => array('search_admin_settings'), 'access arguments' => array('administer search'), 'type' => MENU_NORMAL_ITEM, 'file' => 'search.admin.inc', ); $items['admin/config/search/settings/reindex'] = array( 'title' => 'Clear index', 'page callback' => 'drupal_get_form', 'page arguments' => array('search_reindex_confirm'), 'access arguments' => array('administer search'), 'type' => MENU_CALLBACK, 'file' => 'search.admin.inc', ); $items['admin/reports/search'] = array( 'title' => 'Top search phrases', 'description' => 'View most popular search phrases.', 'page callback' => 'dblog_top', 'page arguments' => array('search'), 'access arguments' => array('access site reports'), 'file path' => drupal_get_path('module', 'dblog'), 'file' => 'dblog.admin.inc', ); drupal_static_reset('search_get_info'); $search_hooks = search_get_info(); foreach(variable_get('search_active_modules', array('node', 'user')) as $module) { if (isset($search_hooks[$module])) { $items['search/' . $search_hooks[$module]['path'] . '/%menu_tail'] = array( 'title' => $search_hooks[$module]['title'], 'page callback' => 'search_view', 'page arguments' => array($module), 'access callback' => '_search_menu_access', 'access arguments' => array($module), 'type' => MENU_LOCAL_TASK, 'file' => 'search.pages.inc', ); } } return $items; } /** * Get information about all available search hooks. */ function search_get_info() { $search_hooks = &drupal_static(__FUNCTION__); if (!isset($search_hooks)) { foreach (module_implements('search_info') as $module) { $search_hooks[$module] = call_user_func($module . '_search_info'); // Use module name as the default. $search_hooks[$module] += array('title' => $module, 'path' => $module); } } return $search_hooks; } /** * Access callback for search tabs. */ function _search_menu_access($name) { return user_access('search content') && (!function_exists($name . '_search_access') || module_invoke($name, 'search_access')); } /** * Wipes a part of or the entire search index. * * @param $sid * (optional) The SID of the item to wipe. If specified, $type must be passed * too. * @param $type * (optional) The type of item to wipe. */ function search_reindex($sid = NULL, $type = NULL, $reindex = FALSE) { if ($type == NULL && $sid == NULL) { module_invoke_all('search_reset'); } else { db_delete('search_dataset') ->condition('sid', $sid) ->condition('type', $type) ->execute(); db_delete('search_index') ->condition('sid', $sid) ->condition('type', $type) ->execute(); // Don't remove links if re-indexing. if (!$reindex) { db_delete('search_node_links') ->condition('sid', $sid) ->condition('type', $type) ->execute(); } } } /** * Marks a word as dirty (or retrieves the list of dirty words). This is used * during indexing (cron). Words which are dirty have outdated total counts in * the search_total table, and need to be recounted. */ function search_dirty($word = NULL) { $dirty = &drupal_static(__FUNCTION__, array()); if ($word !== NULL) { $dirty[$word] = TRUE; } else { return $dirty; } } /** * Implements hook_cron(). * * Fires hook_update_index() in all modules and cleans up dirty words (see * search_dirty). */ function search_cron() { // We register a shutdown function to ensure that search_total is always up // to date. register_shutdown_function('search_update_totals'); foreach(variable_get('search_active_modules', array('node', 'user')) as $module) { // Update word index module_invoke($module, 'update_index'); } } /** * This function is called on shutdown to ensure that search_total is always * up to date (even if cron times out or otherwise fails). */ function search_update_totals() { // Update word IDF (Inverse Document Frequency) counts for new/changed words. foreach (search_dirty() as $word => $dummy) { // Get total count $total = db_query("SELECT SUM(score) FROM {search_index} WHERE word = :word", array(':word' => $word), array('target' => 'slave'))->fetchField(); // Apply Zipf's law to equalize the probability distribution. $total = log10(1 + 1/(max(1, $total))); db_merge('search_total') ->key(array('word' => $word)) ->fields(array('count' => $total)) ->execute(); } // Find words that were deleted from search_index, but are still in // search_total. We use a LEFT JOIN between the two tables and keep only the // rows which fail to join. $result = db_query("SELECT t.word AS realword, i.word FROM {search_total} t LEFT JOIN {search_index} i ON t.word = i.word WHERE i.word IS NULL", array(), array('target' => 'slave')); $or = db_or(); foreach ($result as $word) { $or->condition('word', $word->realword); } if (count($or) > 0) { db_delete('search_total') ->condition($or) ->execute(); } } /** * Simplifies a string according to indexing rules. */ function search_simplify($text) { // Decode entities to UTF-8 $text = decode_entities($text); // Lowercase $text = drupal_strtolower($text); // Call an external processor for word handling. search_invoke_preprocess($text); // Simple CJK handling if (variable_get('overlap_cjk', TRUE)) { $text = preg_replace_callback('/[' . PREG_CLASS_CJK . ']+/u', 'search_expand_cjk', $text); } // To improve searching for numerical data such as dates, IP addresses // or version numbers, we consider a group of numerical characters // separated only by punctuation characters to be one piece. // This also means that searching for e.g. '20/03/1984' also returns // results with '20-03-1984' in them. // Readable regexp: ([number]+)[punctuation]+(?=[number]) $text = preg_replace('/([' . PREG_CLASS_NUMBERS . ']+)[' . PREG_CLASS_PUNCTUATION . ']+(?=[' . PREG_CLASS_NUMBERS . '])/u', '\1', $text); // The dot, underscore and dash are simply removed. This allows meaningful // search behavior with acronyms and URLs. No need to use the unicode modifer // here because 0-127 ASCII characters can't match higher UTF-8 characters as // the leftmost bit of those are 1. $text = preg_replace('/[._-]+/', '', $text); // With the exception of the rules above, we consider all punctuation, // marks, spacers, etc, to be a word boundary. $text = preg_replace('/[' . PREG_CLASS_SEARCH_EXCLUDE . ']+/u', ' ', $text); return $text; } /** * Basic CJK tokenizer. Simply splits a string into consecutive, overlapping * sequences of characters ('minimum_word_size' long). */ function search_expand_cjk($matches) { $min = variable_get('minimum_word_size', 3); $str = $matches[0]; $l = drupal_strlen($str); // Passthrough short words if ($l <= $min) { return ' ' . $str . ' '; } $tokens = ' '; // FIFO queue of characters $chars = array(); // Begin loop for ($i = 0; $i < $l; ++$i) { // Grab next character $current = drupal_substr($str, 0, 1); $str = substr($str, strlen($current)); $chars[] = $current; if ($i >= $min - 1) { $tokens .= implode('', $chars) . ' '; array_shift($chars); } } return $tokens; } /** * Splits a string into tokens for indexing. */ function search_index_split($text) { $last = &drupal_static(__FUNCTION__); $lastsplit = &drupal_static(__FUNCTION__ . ':lastsplit'); if ($last == $text) { return $lastsplit; } // Process words $text = search_simplify($text); $words = explode(' ', $text); array_walk($words, '_search_index_truncate'); // Save last keyword result $last = $text; $lastsplit = $words; return $words; } /** * Helper function for array_walk in search_index_split. */ function _search_index_truncate(&$text) { $text = truncate_utf8($text, 50); } /** * Invokes hook_search_preprocess() in modules. */ function search_invoke_preprocess(&$text) { foreach (module_implements('search_preprocess') as $module) { $text = module_invoke($module, 'search_preprocess', $text); } } /** * Update the full-text search index for a particular item. * * @param $sid * A number identifying this particular item (e.g. node id). * * @param $type * A string defining this type of item (e.g. 'node') * * @param $text * The content of this item. Must be a piece of HTML text. * * @ingroup search */ function search_index($sid, $type, $text) { $minimum_word_size = variable_get('minimum_word_size', 3); // Link matching global $base_url; $node_regexp = '@href=[\'"]?(?:' . preg_quote($base_url, '@') . '/|' . preg_quote(base_path(), '@') . ')(?:\?q=)?/?((?![a-z]+:)[^\'">]+)[\'">]@i'; // Multipliers for scores of words inside certain HTML tags. The weights are stored // in a variable so that modules can overwrite the default weights. // Note: 'a' must be included for link ranking to work. $tags = variable_get('search_tag_weights', array( 'h1' => 25, 'h2' => 18, 'h3' => 15, 'h4' => 12, 'h5' => 9, 'h6' => 6, 'u' => 3, 'b' => 3, 'i' => 3, 'strong' => 3, 'em' => 3, 'a' => 10)); // Strip off all ignored tags to speed up processing, but insert space before/after // them to keep word boundaries. $text = str_replace(array('<', '>'), array(' <', '> '), $text); $text = strip_tags($text, '<' . implode('><', array_keys($tags)) . '>'); // Split HTML tags from plain text. $split = preg_split('/\s*<([^>]+?)>\s*/', $text, -1, PREG_SPLIT_DELIM_CAPTURE); // Note: PHP ensures the array consists of alternating delimiters and literals // and begins and ends with a literal (inserting $null as required). $tag = FALSE; // Odd/even counter. Tag or no tag. $link = FALSE; // State variable for link analyzer $score = 1; // Starting score per word $accum = ' '; // Accumulator for cleaned up data $tagstack = array(); // Stack with open tags $tagwords = 0; // Counter for consecutive words $focus = 1; // Focus state $results = array(0 => array()); // Accumulator for words for index foreach ($split as $value) { if ($tag) { // Increase or decrease score per word based on tag list($tagname) = explode(' ', $value, 2); $tagname = drupal_strtolower($tagname); // Closing or opening tag? if ($tagname[0] == '/') { $tagname = substr($tagname, 1); // If we encounter unexpected tags, reset score to avoid incorrect boosting. if (!count($tagstack) || $tagstack[0] != $tagname) { $tagstack = array(); $score = 1; } else { // Remove from tag stack and decrement score $score = max(1, $score - $tags[array_shift($tagstack)]); } if ($tagname == 'a') { $link = FALSE; } } else { if (isset($tagstack[0]) && $tagstack[0] == $tagname) { // None of the tags we look for make sense when nested identically. // If they are, it's probably broken HTML. $tagstack = array(); $score = 1; } else { // Add to open tag stack and increment score array_unshift($tagstack, $tagname); $score += $tags[$tagname]; } if ($tagname == 'a') { // Check if link points to a node on this site if (preg_match($node_regexp, $value, $match)) { $path = drupal_get_normal_path($match[1]); if (preg_match('!(?:node|book)/(?:view/)?([0-9]+)!i', $path, $match)) { $linknid = $match[1]; if ($linknid > 0) { $node = db_query('SELECT title, nid, vid FROM {node} WHERE nid = :nid', array(':nid' => $linknid), array('target' => 'slave'))->fetchObject(); $link = TRUE; $linktitle = $node->title; } } } } } // A tag change occurred, reset counter. $tagwords = 0; } else { // Note: use of PREG_SPLIT_DELIM_CAPTURE above will introduce empty values if ($value != '') { if ($link) { // Check to see if the node link text is its URL. If so, we use the target node title instead. if (preg_match('!^https?://!i', $value)) { $value = $linktitle; } } $words = search_index_split($value); foreach ($words as $word) { // Add word to accumulator $accum .= $word . ' '; $num = is_numeric($word); // Check wordlength if ($num || drupal_strlen($word) >= $minimum_word_size) { // Normalize numbers if ($num) { $word = (int)ltrim($word, '-0'); } // Links score mainly for the target. if ($link) { if (!isset($results[$linknid])) { $results[$linknid] = array(); } $results[$linknid][] = $word; // Reduce score of the link caption in the source. $focus *= 0.2; } // Fall-through if (!isset($results[0][$word])) { $results[0][$word] = 0; } $results[0][$word] += $score * $focus; // Focus is a decaying value in terms of the amount of unique words up to this point. // From 100 words and more, it decays, to e.g. 0.5 at 500 words and 0.3 at 1000 words. $focus = min(1, .01 + 3.5 / (2 + count($results[0]) * .015)); } $tagwords++; // Too many words inside a single tag probably mean a tag was accidentally left open. if (count($tagstack) && $tagwords >= 15) { $tagstack = array(); $score = 1; } } } } $tag = !$tag; } search_reindex($sid, $type, TRUE); // Insert cleaned up data into dataset db_insert('search_dataset') ->fields(array( 'sid' => $sid, 'type' => $type, 'data' => $accum, 'reindex' => 0, )) ->execute(); // Insert results into search index foreach ($results[0] as $word => $score) { // If a word already exists in the database, its score gets increased // appropriately. If not, we create a new record with the appropriate // starting score. db_merge('search_index') ->key(array( 'word' => $word, 'sid' => $sid, 'type' => $type, )) ->fields(array('score' => $score)) ->expression('score', 'score + :score', array(':score' => $score)) ->execute(); search_dirty($word); } unset($results[0]); // Get all previous links from this item. $result = db_query("SELECT nid, caption FROM {search_node_links} WHERE sid = :sid AND type = :type", array( ':sid' => $sid, ':type' => $type ), array('target' => 'slave')); $links = array(); foreach ($result as $link) { $links[$link->nid] = $link->caption; } // Now store links to nodes. foreach ($results as $nid => $words) { $caption = implode(' ', $words); if (isset($links[$nid])) { if ($links[$nid] != $caption) { // Update the existing link and mark the node for reindexing. db_update('search_node_links') ->fields(array('caption' => $caption)) ->condition('sid', $sid) ->condition('type', $type) ->condition('nid', $nid) ->execute(); search_touch_node($nid); } // Unset the link to mark it as processed. unset($links[$nid]); } else { // Insert the existing link and mark the node for reindexing. db_insert('search_node_links') ->fields(array( 'caption' => $caption, 'sid' => $sid, 'type' => $type, 'nid' => $nid, )) ->execute(); search_touch_node($nid); } } // Any left-over links in $links no longer exist. Delete them and mark the nodes for reindexing. foreach ($links as $nid => $caption) { db_delete('search_node_links') ->condition('sid', $sid) ->condition('type', $type) ->condition('nid', $nid) ->execute(); search_touch_node($nid); } } /** * Change a node's changed timestamp to 'now' to force reindexing. * * @param $nid * The nid of the node that needs reindexing. */ function search_touch_node($nid) { db_update('search_dataset') ->fields(array('reindex' => REQUEST_TIME)) ->condition('type', 'node') ->condition('sid', $nid) ->execute(); } /** * Implements hook_node_update_index(). */ function search_node_update_index($node) { // Transplant links to a node into the target node. $result = db_query("SELECT caption FROM {search_node_links} WHERE nid = :nid", array(':nid' => $node->nid), array('target' => 'slave')); $output = array(); foreach ($result as $link) { $output[] = $link->caption; } if (count($output)) { return '(' . implode(', ', $output) . ')'; } } /** * Implements hook_node_update(). */ function search_node_update($node) { // Reindex the node when it is updated. The node is automatically indexed // when it is added, simply by being added to the node table. search_touch_node($node->nid); } /** * Implements hook_comment_insert(). */ function search_comment_insert($comment) { // Reindex the node when comments are added. search_touch_node($comment->nid); } /** * Implements hook_comment_update(). */ function search_comment_update($comment) { // Reindex the node when comments are changed. search_touch_node($comment->nid); } /** * Implements hook_comment_delete(). */ function search_comment_delete($comment) { // Reindex the node when comments are deleted. search_touch_node($comment->nid); } /** * Implements hook_comment_publish(). */ function search_comment_publish($comment) { // Reindex the node when comments are published. search_touch_node($comment->nid); } /** * Implements hook_comment_unpublish(). */ function search_comment_unpublish($comment) { // Reindex the node when comments are unpublished. search_touch_node($comment->nid); } /** * Extract a module-specific search option from a search query. e.g. 'type:book' */ function search_expression_extract($keys, $option) { if (preg_match('/(^| )' . $option . ':([^ ]*)( |$)/i', $keys, $matches)) { return $matches[2]; } } /** * Return a query with the given module-specific search option inserted in. * e.g. 'type:book'. */ function search_expression_insert($keys, $option, $value = '') { if (search_expression_extract($keys, $option)) { $keys = trim(preg_replace('/(^| )' . $option . ':[^ ]*/i', '', $keys)); } if ($value != '') { $keys .= ' ' . $option . ':' . $value; } return $keys; } /** * Helper function for grabbing search keys. */ function search_get_keys() { $return = &drupal_static(__FUNCTION__); if (!isset($return)) { // Extract keys as remainder of path // Note: support old GET format of searches for existing links. $path = explode('/', $_GET['q'], 3); $keys = empty($_REQUEST['keys']) ? '' : $_REQUEST['keys']; $return = count($path) == 3 ? $path[2] : $keys; } return $return; } /** * @defgroup search Search interface * @{ * The Drupal search interface manages a global search mechanism. * * Modules may plug into this system to provide searches of different types of * data. Most of the system is handled by search.module, so this must be enabled * for all of the search features to work. * * There are three ways to interact with the search system: * - Specifically for searching nodes, you can implement hook_node_update_index() * and hook_node_search_result(). However, note that the search system already * indexes all visible output of a node, i.e. everything displayed normally * by hook_view() and hook_node_view(). This is usually sufficient. You should * only use this mechanism if you want additional, non-visible data to be * indexed. * - Implement hook_search_info(). This will create a search tab for your module on * the /search page with a simple keyword search form. You will also need to * implement hook_search_execute() to perform the search. * - Implement hook_update_index(). This allows your module to use Drupal's * HTML indexing mechanism for searching full text efficiently. * * If your module needs to provide a more complicated search form, then you need * to implement it yourself without hook_search(). In that case, you should * define it as a local task (tab) under the /search page (e.g. /search/mymodule) * so that users can easily find it. */ /** * Render a search form. * * @param $action * Form action. Defaults to "search". * @param $keys * The search string entered by the user, containing keywords for the search. * @param $type * The type of search to render the node for. Must be the name of module * which implements hook_search(). Defaults to 'node'. * @param $prompt * A piece of text to put before the form (e.g. "Enter your keywords") * @return * An HTML string containing the search form. */ function search_form($form, &$form_state, $action = '', $keys = '', $type = NULL, $prompt = NULL) { // Add CSS drupal_add_css(drupal_get_path('module', 'search') . '/search.css', array('preprocess' => FALSE)); if (!$action) { $action = url('search/' . $type); } if (is_null($prompt)) { $prompt = t('Enter your keywords'); } $form['#action'] = $action; $form['#attributes']['class'][] = 'search-form'; $form['module'] = array('#type' => 'value', '#value' => $type); $form['basic'] = array('#type' => 'item', '#title' => $prompt, '#id' => 'edit-keys'); $form['basic']['inline'] = array('#prefix' => '
', '#suffix' => '
'); $form['basic']['inline']['keys'] = array( '#type' => 'textfield', '#title' => '', '#default_value' => $keys, '#size' => $prompt ? 40 : 20, '#maxlength' => 255, ); // processed_keys is used to coordinate keyword passing between other forms // that hook into the basic search form. $form['basic']['inline']['processed_keys'] = array('#type' => 'value', '#value' => array()); $form['basic']['inline']['submit'] = array('#type' => 'submit', '#value' => t('Search')); return $form; } /** * Form builder; Output a search form for the search block's search box. * * @ingroup forms * @see search_box_form_submit() * @see search-theme-form.tpl.php * @see search-block-form.tpl.php */ function search_box($form, &$form_state, $form_id) { $form[$form_id] = array( '#type' => 'textfield', '#size' => 15, '#default_value' => '', '#attributes' => array('title' => t('Enter the terms you wish to search for.')), ); $form['submit'] = array('#type' => 'submit', '#value' => t('Search')); $form['#submit'][] = 'search_box_form_submit'; return $form; } /** * Process a block search form submission. */ function search_box_form_submit($form, &$form_state) { // The search form relies on control of the redirect destination for its // functionality, so we override any static destination set in the request, // for example by drupal_access_denied() or drupal_not_found() // (see http://drupal.org/node/292565). if (isset($_GET['destination'])) { unset($_GET['destination']); } $form_id = $form['form_id']['#value']; $form_state['redirect'] = 'search/node/' . trim($form_state['values'][$form_id]); } /** * Process variables for search-block-form.tpl.php. * * The $variables array contains the following arguments: * - $form * * @see search-block-form.tpl.php */ function template_preprocess_search_block_form(&$variables) { $variables['search'] = array(); $hidden = array(); // Provide variables named after form keys so themers can print each element independently. foreach (element_children($variables['form']) as $key) { $type = $variables['form'][$key]['#type']; if ($type == 'hidden' || $type == 'token') { $hidden[] = drupal_render($variables['form'][$key]); } else { $variables['search'][$key] = drupal_render($variables['form'][$key]); } } // Hidden form elements have no value to themers. No need for separation. $variables['search']['hidden'] = implode($hidden); // Collect all form elements to make it easier to print the whole form. $variables['search_form'] = implode($variables['search']); } /** * Perform a standard search on the given keys, and return the formatted results. */ function search_data($keys = NULL, $type = 'node') { if (isset($keys)) { if (module_hook($type, 'search_execute')) { $results = module_invoke($type, 'search_execute', $keys); if (module_hook($type, 'search_page')) { return module_invoke($type, 'search_page', $results); } else { return theme('search_results', array('results' => $results, 'type' => $type)); } } } } /** * Returns snippets from a piece of text, with certain keywords highlighted. * Used for formatting search results. * * @param $keys * A string containing a search query. * * @param $text * The text to extract fragments from. * * @return * A string containing HTML for the excerpt. */ function search_excerpt($keys, $text) { // We highlight around non-indexable or CJK characters. $boundary = '(?:(?<=[' . PREG_CLASS_SEARCH_EXCLUDE . PREG_CLASS_CJK . '])|(?=[' . PREG_CLASS_SEARCH_EXCLUDE . PREG_CLASS_CJK . ']))'; // Extract positive keywords and phrases preg_match_all('/ ("([^"]+)"|(?!OR)([^" ]+))/', ' ' . $keys, $matches); $keys = array_merge($matches[2], $matches[3]); // Prepare text $text = ' ' . strip_tags(str_replace(array('<', '>'), array(' <', '> '), $text)) . ' '; array_walk($keys, '_search_excerpt_replace'); $workkeys = $keys; // Extract a fragment per keyword for at most 4 keywords. // First we collect ranges of text around each keyword, starting/ending // at spaces. // If the sum of all fragments is too short, we look for second occurrences. $ranges = array(); $included = array(); $length = 0; while ($length < 256 && count($workkeys)) { foreach ($workkeys as $k => $key) { if (strlen($key) == 0) { unset($workkeys[$k]); unset($keys[$k]); continue; } if ($length >= 256) { break; } // Remember occurrence of key so we can skip over it if more occurrences // are desired. if (!isset($included[$key])) { $included[$key] = 0; } // Locate a keyword (position $p), then locate a space in front (position // $q) and behind it (position $s) if (preg_match('/' . $boundary . $key . $boundary . '/iu', $text, $match, PREG_OFFSET_CAPTURE, $included[$key])) { $p = $match[0][1]; if (($q = strpos($text, ' ', max(0, $p - 60))) !== FALSE) { $end = substr($text, $p, 80); if (($s = strrpos($end, ' ')) !== FALSE) { $ranges[$q] = $p + $s; $length += $p + $s - $q; $included[$key] = $p + 1; } else { unset($workkeys[$k]); } } else { unset($workkeys[$k]); } } else { unset($workkeys[$k]); } } } // If we didn't find anything, return the beginning. if (count($ranges) == 0) { return truncate_utf8($text, 256) . ' ...'; } // Sort the text ranges by starting position. ksort($ranges); // Now we collapse overlapping text ranges into one. The sorting makes it O(n). $newranges = array(); foreach ($ranges as $from2 => $to2) { if (!isset($from1)) { $from1 = $from2; $to1 = $to2; continue; } if ($from2 <= $to1) { $to1 = max($to1, $to2); } else { $newranges[$from1] = $to1; $from1 = $from2; $to1 = $to2; } } $newranges[$from1] = $to1; // Fetch text $out = array(); foreach ($newranges as $from => $to) { $out[] = substr($text, $from, $to - $from); } $text = (isset($newranges[0]) ? '' : '... ') . implode(' ... ', $out) . ' ...'; // Highlight keywords. Must be done at once to prevent conflicts ('strong' and ''). $text = preg_replace('/' . $boundary . '(' . implode('|', $keys) . ')' . $boundary . '/iu', '\0', $text); return $text; } /** * @} End of "defgroup search". */ /** * Helper function for array_walk in search_except. */ function _search_excerpt_replace(&$text) { $text = preg_quote($text, '/'); } function search_forms() { $forms['search_block_form']= array( 'callback' => 'search_box', 'callback arguments' => array('search_block_form'), ); return $forms; }