PRINT_PATH, 'title' => t('Printer-friendly'), 'callback' => 'print_controller', 'access' => user_access('access print'), 'type' => MENU_CALLBACK ); $items[] = array( 'path' => 'admin/settings/print', 'title' => t('Printer-friendly'), 'description' => t('Adds a printer-friendly version link to node pages.'), 'callback' => 'drupal_get_form', 'callback arguments' => array('print_main_settings'), 'access' => user_access('administer print'), ); } return $items; } /** * Implementation of hook_link(). */ function print_link($type, $node = NULL, $teaser = FALSE) { $print_settings = variable_get('print_settings', print_settings_default()); // No link is shown for several motives... if ( !($teaser) && ($node->type != 'book') && (!$node->parent) && ($print_settings['show_link']) && user_access('access print') && (($type == 'comment' && variable_get('print_display_comment', '0')) || ($type == 'node' && variable_get('print_display_'. $node->type, '1')))) { $links = array(); $format = theme('print_format_link'); if ($type == 'comment') { $query = "comment=$node->cid"; } $links['print'] = array('href' => PRINT_PATH ."/". $node->nid, 'title' => $format['text'], 'attributes' => $format['attributes'], 'html' => $format['html'], 'query' => $query); return $links; } else { return; } } /** * Implementation of hook_link_alter(). */ function print_link_alter(&$node, &$links) { foreach ($links AS $module => $link) { if (strstr($module, 'book_printer')) { $print_settings = variable_get('print_settings', print_settings_default()); if ($print_settings['book_link']) { $format = theme('print_format_link'); $format['attributes']['title'] = $link['attributes']['title']; $links[$module]['href'] = PRINT_PATH ."/". $link['href']; $links[$module]['attributes'] = $format['attributes']; } } } } /** * Implementation of hook_help() */ function print_help($path) { $print_settings = variable_get('print_settings', print_settings_default()); if (($print_settings['show_link']) && ($print_settings['show_sys_link']) && user_access('access print') && (preg_match("/^node\//i", $path) == 0)) { static $output = FALSE; if ($output === FALSE) { $output = TRUE; return print_insert_link(); } } } /** * Implementation of hook_form_alter() */ function print_form_alter($form_id, &$form) { // Add the node-type settings option to activate the printer-friendly version link if ('node_type_form' == $form_id) { $form['workflow']['print_display'] = array( '#type' => 'checkbox', '#title' => t('Show printer-friendly version link'), '#return_value' => 1, '#default_value' => variable_get('print_display_'. $form['#node_type']->type, '1'), '#description' => t('Displays the link to a printer-friendly version of the content. Further configuration is available on the !settings.', array('!settings' => l(t('settings page'), 'admin/settings/print' ))), ); } elseif ('comment_admin_settings' == $form_id) { $form['viewing_options']['print_display_comment'] = array( '#type' => 'checkbox', '#title' => t('Show printer-friendly version link in individual comments'), '#return_value' => 1, '#default_value' => variable_get('print_display_comment', '0'), '#description' => t('Displays the link to a printer-friendly version of the comment. Further configuration is available on the !settings.', array('!settings' => l(t('settings page'), 'admin/settings/print' ))), ); } } //******************************************************************* // Admin Settings: Print //******************************************************************* /** * Default values of the print_settings variable */ function print_settings_default() { return array('show_link' => 1, 'show_sys_link' => 1, 'book_link' => 1, 'logo_url' => '', 'css' => '', 'urls' => 1, 'comments' => 0, 'newwindow' => 0, 'sendtoprinter' => 0); } /** * Default values of the print_sourceurl_settings variable */ function print_sourceurl_settings_default() { return array('enabled' => 1, 'date' => 0, 'forcenode' => 0); } /** * Default values of the print_robot_settings variable */ function print_robot_settings_default() { return array('noindex' => 1, 'nofollow' => 1, 'noarchive' => 0, 'nocache' => 0); } /** * Print module settings form */ function print_main_settings() { $print_settings = variable_get('print_settings', print_settings_default()); $form['print_settings'] = array( '#type' => 'fieldset', '#tree' => TRUE, ); $form['print_settings']['show_link'] = array( '#type' => 'radios', '#title' => t('Printer-friendly page link'), '#default_value' => $print_settings['show_link'], '#options' => array(t("Disabled"), t("Enabled")), '#description' => t("Enable or disable the printer-friendly page link for each node. Even if the link is disabled, you can still view the print version of a node by going to print/nid where nid is the numeric id of the node."), ); $form['print_settings']['show_sys_link'] = array( '#type' => 'checkbox', '#title' => t('Show link in system (non-content) pages'), '#return_value' => 1, '#default_value' => $print_settings['show_sys_link'], '#description' => t('Setting this option will add a printer-friendly version page link on pages created by Drupal or the enabled modules.'), ); $form['print_settings']['book_link'] = array( '#type' => 'checkbox', '#title' => t('Take control of the book module printer-friendly link'), '#return_value' => 1, '#default_value' => $print_settings['book_link'], '#description' => t('Activate this to have the printer-friendly link in book nodes handled by this module. Requires the (core) book module.'), ); $form['print_settings']['logo_url'] = array( '#type' => 'textfield', '#title' => t('Logo URL'), '#default_value' => $print_settings['logo_url'], '#size' => 60, '#maxlength' => 250, '#description' => t('An alternative logo to display on the printer-friendly version. If left empty, the current theme\'s logo is used.'), ); $form['print_settings']['css'] = array( '#type' => 'textfield', '#title' => t('Stylesheet URL'), '#default_value' => $print_settings['css'], '#size' => 60, '#maxlength' => 64, '#description' => t('The URL to your custom print cascading stylesheet, if any. When none is specified, the default module CSS file is used.'), ); $form['print_settings']['urls'] = array( '#type' => 'checkbox', '#title' => t('Printer-friendly URLs list'), '#return_value' => 1, '#default_value' => $print_settings['urls'], '#description' => t('If set, a list of the destination URLs for the page links will be displayed at the bottom of the page.'), ); $form['print_settings']['comments'] = array( '#type' => 'checkbox', '#title' => t('Include comments in printer-friendly version'), '#return_value' => 1, '#default_value' => $print_settings['comments'], '#description' => t('When this option is active, user comments are also included in the printer-friendly version. Requires the comment module.'), ); $form['print_settings']['newwindow'] = array( '#type' => 'radios', '#title' => t('Open the printer-friendly version in a new window'), '#options' => array(t("Disabled"), t("Use HTML target (does not validate as XHTML Strict)"), t("Use Javascript (requires browser support)")), '#default_value' => $print_settings['newwindow'], '#description' => t('Setting this option will make the printer-friendly version open in a new window/tab.'), ); $form['print_settings']['sendtoprinter'] = array( '#type' => 'checkbox', '#title' => t('Send to printer'), '#return_value' => 1, '#default_value' => $print_settings['sendtoprinter'], '#description' => t('Automatically calls the browser\'s print function when the printer-friendly version is displayed.'), ); $print_sourceurl_settings = variable_get('print_sourceurl_settings', print_sourceurl_settings_default()); $form['print_sourceurl_settings'] = array( '#type' => 'fieldset', '#title' => t('Source URL'), '#collapsible' => true, '#collapsed' => true, '#tree' => true, ); $form['print_sourceurl_settings']['enabled'] = array( '#type' => 'checkbox', '#title' => t('Display source URL'), '#return_value' => 1, '#default_value' => $print_sourceurl_settings['enabled'], '#description' => t('When this option is selected, the URL for the original page will be displayed at the bottom of the printer-friendly version.'), ); $form['print_sourceurl_settings']['date'] = array( '#type' => 'checkbox', '#title' => t('Add current time/date to the source URL'), '#return_value' => 1, '#default_value' => $print_sourceurl_settings['date'], '#description' => t('Display the current date and time in the Source URL line.'), ); $form['print_sourceurl_settings']['forcenode'] = array( '#type' => 'checkbox', '#title' => t('Force use of node ID in source URL'), '#return_value' => 1, '#default_value' => $print_sourceurl_settings['forcenode'], '#description' => t('Drupal will attempt to use the page\'s defined alias in case there is one. To force the use of the fixed URL, activate this option.'), ); $print_robot_settings = variable_get('print_robot_settings', print_robot_settings_default()); $form['print_robot_settings'] = array( '#type' => 'fieldset', '#title' => t('Robots META tags'), '#collapsible' => true, '#collapsed' => true, '#tree' => TRUE, ); $form['print_robot_settings']['noindex'] = array( '#type' => 'checkbox', '#title' => t('Add noindex'), '#return_value' => 1, '#default_value' => $print_robot_settings['noindex'], '#description' => t('Instruct robots to not index printer-friendly pages. Recommended for good search engine karma.') ); $form['print_robot_settings']['nofollow'] = array( '#type' => 'checkbox', '#title' => t('Add nofollow'), '#return_value' => 1, '#default_value' => $print_robot_settings['nofollow'], '#description' => t('Instruct robots to not follow outgoing links on printer-friendly pages.') ); $form['print_robot_settings']['noarchive'] = array( '#type' => 'checkbox', '#title' => t('Add noarchive'), '#return_value' => 1, '#default_value' => $print_robot_settings['noarchive'], '#description' => t('Non-standard tag to instruct search engines to not show a "Cached" link for your printer-friendly pages. Recognized by Googlebot.') ); $form['print_robot_settings']['nocache'] = array( '#type' => 'checkbox', '#title' => t('Add nocache'), '#return_value' => 1, '#default_value' => $print_robot_settings['nocache'], '#description' => t('Non-standard tag to instruct search engines to not show a "Cached" link for your printer-friendly pages') ); return system_settings_form($form); } //******************************************************************* // Module Functions :: Controller //******************************************************************* /** * Print module path catcher */ function print_controller() { // Remove the print/ prefix $args = substr($_GET['q'], strlen(PRINT_PATH)+1); $cid = $_GET['comment']; $nid = $args; if (!is_numeric($nid)) { // Indirect call with print/alias // If there is a path alias with these arguments, generate a printer-friendly version for it $path = drupal_get_normal_path($args); $ret = preg_match("/^node\/(.*)/i", $path, $matches); if ($ret == 1) { $nid = $matches[1]; } } if (is_numeric($nid)) { print_generate_node($nid, $cid); } else { $ret = preg_match("/^book\/export\/html\/(.*)/i", $args, $matches); if ($ret == 1) { // This is a book PF page link, handle trough the book handling functions print_generate_book($matches[1]); } else { // If no content node was found, handle the page printing with the 'printable' engine print_generate_path($args); } } } //******************************************************************* // Module Functions : Auxiliary //******************************************************************* /** * Generates a meta tag to tell robots what they may index based on module settings * * @return string */ function _print_robots_meta_generator() { $robots_settings = variable_get('print_robot_settings', print_robot_settings_default()); $robots_meta = array(); if (!empty($robots_settings['noindex'])) { $robots_meta[] = 'noindex'; } if (!empty($robots_settings['nofollow'])) { $robots_meta[] = 'nofollow'; } if (!empty($robots_settings['noarchive'])) { $robots_meta[] = 'noarchive'; } if (!empty($robots_settings['nocache'])) { $robots_meta[] = 'nocache'; } if (sizeof($robots_meta) > 0) { $robots_meta = isset($robots_meta[1]) ? implode(', ', $robots_meta) : $robots_meta[0]; $robots_meta = '\n"; } else { $robots_meta = ''; } return $robots_meta; } /** * Generates the HTML to insert in the template file * * @return string */ function _print_var_generator($node, $cid = NULL) { global $base_url, $base_path; $path = empty($node->nid) ? $node->path : "node/$node->nid"; $themed = theme('print_text'); // print module settings $print_settings = variable_get('print_settings', print_settings_default()); $print_sourceurl_settings = variable_get('print_sourceurl_settings', print_sourceurl_settings_default()); $print["language"] = $GLOBALS['locale']; $print["title"] = $node->title; $print["robots_meta"] = _print_robots_meta_generator(); $print["base_href"] = "\n"; $print["head"] = drupal_get_html_head(); $print["scripts"] = drupal_get_js(); $print["favicon"] = theme_get_setting("toggle_favicon") ? "\n" : ""; if (!empty($print_settings['css'])) { $css_file = $print_settings['css']; } else { $css_file = $base_path . drupal_get_path('module', 'print') ."/print.css"; } $print["css"] = "\n"; $print["sendtoprinter"] = $print_settings['sendtoprinter'] ? " onload=\"window.print();\"" : ""; $print["logo"] = !empty($print_settings['logo_url']) ? $print_settings['logo_url'] : theme_get_setting('logo'); $print["logo"] = $print["logo"] ? "\"\"\n" : ""; /* Grab and format the src URL */ if ($print_sourceurl_settings['enabled'] == 1) { if (empty($print_sourceurl_settings['forcenode'])) { $print["source_url"] = url($path, NULL, NULL, TRUE); } else { $print["source_url"] = $base_url .'/'. (((bool)variable_get('clean_url', '0')) ? '' : '?q=') . $path; } if ($cid) { $print["source_url"] .= "#comment-$cid"; } $print["printdate"] = $print_sourceurl_settings['date'] ? (" (". $themed['retrieved'] ." ". format_date(time(), 'small') .")") : ""; $print["source_url"] = "". $themed['sourceURL'] . $print["printdate"] .": ". $print["source_url"] .""; } else { $print["source_url"] = ""; } $print["site_name"] = variable_get('site_name', 0) ? ($themed['published'] ." ". variable_get('site_name', 0) ." (". l($base_url, $base_url) .")") : ""; $print["submitted"] = theme_get_setting("toggle_node_info_$node->type") ? $themed['by'] ." ". ($node->name ? $node->name : variable_get('anonymous', t('Anonymous'))) : ""; $print["created"] = theme_get_setting("toggle_node_info_$node->type") ? $themed['created'] ." ". format_date($node->created, 'small') : ""; menu_set_active_item($path); $breadcrumb = drupal_get_breadcrumb(); if (!empty($breadcrumb)) { $breadcrumb[] = menu_get_active_title(); $print["breadcrumb"] = implode(" > ", $breadcrumb); } else { $print["breadcrumb"] = ""; } // Display the collected links at the bottom of the page. Code once taken from Kjartan Mannes' project.module if (!empty($print_settings['urls'])) { $urls = print_friendly_urls(); $max = count($urls); if ($max) { $print["pfp_links"] = ''; for ($i = 0; $i < $max; $i++) { $print["pfp_links"] .= '['. ($i + 1) .'] '. $urls[$i] ."
\n"; } $print["pfp_links"] = "

". $themed['links'] ."
". $print["pfp_links"] ."

"; } } $print["footer_message"] = filter_xss_admin(variable_get('site_footer', FALSE)) ."\n". theme('blocks', 'footer') ; if (module_exists('taxonomy')) { $terms = taxonomy_link('taxonomy terms', $node); $print["taxonomy"] = theme('links', $terms); } return $print; } /** * We need to manipulate URLs so that they are recorded as absolute in the * Printer-friendly URLs list, and to add a [n] footnote marker. * * @return string containing the changed tag */ function print_rewrite_urls($matches) { global $base_url, $base_root; // Get value of Printer-friendly URLs setting $print_settings = variable_get('print_settings', print_settings_default()); $pfurls = (!empty($print_settings['urls'])); //Temporarily convert spaces to %20 so that it isn't split below $in_string = false; for ($i=0; $i < strlen($matches[1]); $i++) { if ($matches[1][$i] == '"') { $in_string = !$in_string; } if (($matches[1][$i] == ' ') && ($in_string)) { $matches[1]=substr_replace($matches[1], "%20", $i, 1); } } // remove whitespace immediately before and after the '=' sign $matches[1]=preg_replace("/\s*=\s*/", "=", $matches[1]); // first, split the html into the different tag attributes $attribs = preg_split("/\s+/m", $matches[1]); for ($i=1; $i < count($attribs); $i++) { // If the attribute is href or src, we may need to rewrite the URL in the value if (preg_match("/^(href|src)\s*?=/i", $attribs[$i]) > 0) { // We may need to rewrite the URL, so let's isolate it preg_match("/.*?=(.*)/is", $attribs[$i], $urls); $url = trim($urls[1], " \t\n\r\0\x0B\"\'"); if (strpos($url, '://') || preg_match("/^mailto:.*?@.*?\..*?$/iu", $url)) { // URL is absolute, do nothing $newurl = urldecode($url); } else { if (substr($url, 0, 1) == "#") { // URL is an anchor tag if ($pfurls) { $path = substr($_GET['q'], strlen(PRINT_PATH)+1); if (is_numeric($path)) { $path = "node/$path"; } // Printer-friendly URLs is on, so we need to make it absolute $newurl = url($path, NULL, substr(urldecode($url), 1), TRUE); } // Because base href is the original page, change the link to // still be usable inside the print page $matches[1] = str_replace($url, $_GET['q'] . $url, $matches[1]); } else { // URL is relative, convert it into absolute URL $clean_url = (bool)variable_get('clean_url', '0'); if (substr($url, 0, 1) == "/") { // If it starts with '/' just append it to the server name $newurl = $base_root .'/'. trim(urldecode($url), "/"); } elseif ((!$clean_url) && (preg_match("/^[index.php]?\?q=.*/i", $url))) { // If Clean URLs is disabled, and it starts with q=?, just prepend with the base URL $newurl = $base_url .'/'. trim(urldecode($url), "/"); } else { $newurl = url(trim(urldecode($url), "/"), NULL, NULL, TRUE); } $matches[1] = str_replace($url, $newurl, $matches[1]); } } } } //Revert all %20 in strings back to spaces $matches[1] = str_replace("%20", " ", $matches[1]); $ret = '<'. $matches[1] .'>'; if ($attribs[0] == "a") { $ret .= $matches[2] .''; if (($pfurls) && ($newurl)) { $ret .= ' ['. print_friendly_urls(trim(stripslashes($newurl))) .']'; } } return $ret; } /** * Auxiliary function to store the Printer-friendly URLs list as static. * * @return string containing the list of URLs previously stored if $url is 0, * or the current count otherwise. */ function print_friendly_urls($url = 0) { static $urls = array(); if ($url) { $url_idx = array_search($url, $urls); if ($url_idx !== FALSE) { return ($url_idx + 1); } else { $urls[] = $url; return count($urls); } } return $urls; } /** * Auxiliary function to fill the Printer-friendly link attributes * * @return array of formatted attributes */ function print_fill_attributes() { $print_settings = variable_get('print_settings', print_settings_default()); $robots_settings = variable_get('print_robot_settings', print_robot_settings_default()); $attributes = array('title' => t('Display a printer-friendly version of this page.'), 'class' => 'print-page'); switch ($print_settings['newwindow']) { case 1: $attributes['target'] = '_blank'; break; case 2: $attributes['onclick'] = 'window.open(this.href); return false'; break; } if (!empty($robots_settings['nofollow'])) { $attributes['rel'] = 'nofollow'; } return $attributes; } function theme_print_format_link() { return array('text' => t('Printer-friendly version'), 'html' => FALSE, 'attributes' => print_fill_attributes()); } function theme_print_text() { return array('retrieved' => t('retrieved on'), 'sourceURL' => t('Source URL'), 'published' => t('Published on'), 'by' => t('By'), 'created' => t('Created'), 'links' => t('Links:')); } /** * Auxiliary function to display a formatted Printer-friendly link * * @return string */ function print_insert_link($path = NULL) { if (user_access('access print')) { if ($path === NULL) { $path = PRINT_PATH ."/". $_GET['q']; $query = drupal_query_string_encode($_GET, array('q')); if (empty($query)) { $query = NULL; } } $format = theme('print_format_link'); return ''. l($format['text'], $path, $format['attributes'], $query, NULL, TRUE, $format['html']) .''; } } /** * Auxiliary function to resolve the most appropriate template trying to find * a content specific template in the theme or module dir before falling back * on a generic template also in those dirs. * * @return string with the most suitable template filename */ function print_get_template($type = NULL) { if ($type) { // If the node type is known, then try to find that type's template file // First in the theme directory $filename = drupal_get_path('theme', $GLOBALS['theme_key']) ."/print.node-$type.tpl.php"; if (file_exists($filename)) { return $filename; } $filename = drupal_get_path('theme', $GLOBALS['theme_key']) ."/print.$type.tpl.php"; if (file_exists($filename)) { return $filename; } // Then in the module directory $filename = drupal_get_path('module', 'print') ."/print.node-$type.tpl.php"; if (file_exists($filename)) { return $filename; } $filename = drupal_get_path('module', 'print') ."/print.$type.tpl.php"; if (file_exists($filename)) { return $filename; } } // Search for a generic template file // First in the theme directory $filename = drupal_get_path('theme', $GLOBALS['theme_key']) ."/print.tpl.php"; if (file_exists($filename)) { return $filename; } // Then in the module directory // This one must always exist (provided with the module!) return drupal_get_path('module', 'print') ."/print.tpl.php"; } //******************************************************************* // Module Functions : Content renderers //******************************************************************* /** * Outputs a printer-friendly page. Used for content types */ function print_generate_node($nid, $cid = NULL) { global $base_url; // We can take a node id $node = node_load(array('nid' => $nid)); if (!node_access('view', $node)) { // Access is denied return drupal_access_denied(); } //alert other modules that we are generating a printer-friendly page, so they can choose to show/hide info $node->printing = true; // Turn off Pagination by the Paging module unset($node->pages); unset($node->pages_count); unset($node->page_count); unset($node->teaser); $node = (object)$node; if ($cid === NULL) { // Adapted (simplified) version of node_view for Drupal 5.x //Render the node content $node = node_build_content($node, false, true); // Disable fivestar widget output unset($node->content["fivestar_widget"]); // Disable service links module output unset($node->content["service_links"]); $node->body = drupal_render($node->content); } $print_settings = variable_get('print_settings', print_settings_default()); if (function_exists(comment_render) && (($cid != NULL) || ($print_settings['comments']))) { //Print only the requested comment (or if $cid is NULL, all of them) $comments = comment_render($node, $cid); //Remove the comment forms $comments = preg_replace("/.*?<\/form>/sim", "", $comments); //Remove the 'Post new comment' title $comments = preg_replace("/Post new comment<\/h2>/", "", $comments); //Remove the comment title hyperlink $comments = preg_replace("/()()(.*?)<\/a>(<\/h3>)/", "$1$3$4", $comments); //Remove the comment links $comments = preg_replace("/\s*
.*?<\/div>/sim", "", $comments); if ($cid != NULL) { // Single comment requested, output only the comment unset($node->body); } $node->body .= $comments; } node_invoke_nodeapi($node, 'alter', false, true); // Convert the a href elements $pattern = "@<(a\s[^>]*?)>(.*?)@is"; $node->body = preg_replace_callback($pattern, "print_rewrite_urls", $node->body); init_theme(); $print = _print_var_generator($node, $cid); include_once(print_get_template($node->type)); } /** * Outputs a printer-friendly page. Used for drupal core pages. */ function print_generate_path($path) { global $base_url; $path = drupal_get_normal_path($path); menu_set_active_item($path); // Adapted from index.php. $node = new stdClass(); $node->body = menu_execute_active_handler(); $node->title = menu_get_active_title(); $node->path = $path; // It may happen that a drupal_not_found is called in the above call if (preg_match('/404 Not Found/', drupal_get_headers()) == 1) { return; } switch ($node->body) { case MENU_NOT_FOUND: return drupal_not_found(); break; case MENU_ACCESS_DENIED: return drupal_access_denied(); break; } // Delete any links area $node->body = preg_replace("/\s*
.*?<\/div>/sim", "", $node->body); // Convert the a href elements $pattern = "@<(a\s[^>]*?)>(.*?)@is"; $node->body = preg_replace_callback($pattern, "print_rewrite_urls", $node->body); init_theme(); $print = _print_var_generator($node); include_once(print_get_template()); } /** * Outputs a printer-friendly page. Used for book pages */ function print_generate_book($nid) { global $base_url; $node = node_load(array('nid' => $nid)); if (!node_access('view', $node) || (!user_access('see printer-friendly version'))) { // Access is denied return drupal_access_denied(); } $depth = count(book_location($node)) + 1; $node->body = book_recurse($nid, $depth, 'print_node_visitor_html_pre', 'book_node_visitor_html_post'); // Convert the a href elements $pattern = "@<(a\s[^>]*?)>(.*?)@is"; $node->body = preg_replace_callback($pattern, "print_rewrite_urls", $node->body); init_theme(); $print = _print_var_generator($node); // The title is already displayed by the book_recurse, so avoid duplication $print["title"] = ""; include_once(print_get_template($node->type)); } /** * My own version of book_node_visitor_html_pre so that CCK pages in book nodes * come out right */ function print_node_visitor_html_pre($node, $depth, $nid) { $node = node_build_content($node, false, true); unset($node->content["book_navigation"]); $output .= "
nid ."\" class=\"section-$depth\">\n"; $output .= "

". check_plain($node->title) ."

\n"; $output .= drupal_render($node->content); return $output; }