1<?php 2if(!defined('DOKU_INC')) die(); 3use dokuwiki\File\PageResolver; 4 5/** 6 * Auto-Tooltip DokuWiki plugin 7 * 8 * @license MIT 9 * @author Eli Fenton 10 */ 11class helper_plugin_autotooltip extends DokuWiki_Plugin { 12 private $localRenderer; 13 private static $metaCache = []; 14 15 public function __construct() { 16 $this->localRenderer = new Doku_Renderer_xhtml; 17 } 18 19 20 /** 21 * Return methods of this helper 22 * 23 * @return array with methods description 24 */ 25 public function getMethods() { 26 $result = array(); 27 $result[] = array( 28 'name' => 'forText', 29 'desc' => 'Manually construct a tooltip', 30 'params' => array( 31 'content' => 'string', 32 'tooltip' => 'string', 33 'title (optional)' => 'string', 34 'preTitle (optional)' => 'string', 35 'classes (optional)' => 'string', 36 'textClasses (optional)' => 'string', 37 ), 38 'return' => array('result' => 'string') 39 ); 40 $result[] = array( 41 'name' => 'forWikilink', 42 'desc' => 'Generate a tooltip from a wikilink', 43 'params' => array( 44 'id' => 'string', 45 'content (optional)' => 'string', 46 'preTitle (optional)' => 'string', 47 'classes (optional)' => 'string', 48 'textClasses (optional)' => 'string', 49 ), 50 'return' => array('result' => 'string') 51 ); 52 return $result; 53 } 54 55 56 /** 57 * Return a simple tooltip. 58 * 59 * @param string $content - The on-page content. May contain newlines. 60 * @param string $tooltip - The tooltip content. Newlines will be rendered as line breaks. 61 * @param string $title - The title inside the tooltip. 62 * @param string $preTitle - Text to display before the title. Newlines will be rendered as line breaks. 63 * @param string $classes - CSS classes to add to this tooltip. 64 * @param string $textClasses - CSS classes to add to the linked text. 65 * @param string $link - Link to this external URL. TODO: It would be better to parse arbitrary wikitext 66 * in $content, but that requires a refactor to use $renderer instead of generated HTML. 67 * @return string 68 */ 69 public function forText($content, $tooltip, $title='', $preTitle = '', $classes = '', $textClasses = '', $link = '') { 70 if (empty($classes)) { 71 $classes = $this->getConf('style'); 72 } 73 if (empty($classes)) { 74 $classes = 'default'; 75 } 76 $delay = $this->getConf('delay') ?: 0; 77 78 // Sanitize 79 $classes = htmlspecialchars($classes); 80 // Add the plugin prefix to all classes. 81 $classes = preg_replace('/(\w+)/', 'plugin-autotooltip__$1', $classes); 82 83 $partCount = (empty($title) ? 0 : 1) + (empty($preTitle) ? 0 : 1) + (empty($tooltip) ? 0 : 1); 84 if ($partCount > 1 || strchr($tooltip, "\n") !== FALSE || strlen($tooltip) > 40) { 85 $classes .= ' plugin-autotooltip_big'; 86 } 87 88 if (empty($textClasses)) { 89 $textClasses = 'plugin-autotooltip_linked'; 90 if (strstr($content, '<a ') === FALSE) { 91 $textClasses .= ' plugin-autotooltip_simple'; 92 } 93 } 94 95 $contentParts = []; 96 if (!empty($preTitle)) { 97 $contentParts[] = $this->_formatTT($preTitle); 98 } 99 if (!empty($title)) { 100 $contentParts[] = '<span class="plugin-autotooltip-title">' . $title . '</span>'; 101 } 102 if (!empty($tooltip)) { 103 $contentParts[] = $this->_formatTT($tooltip); 104 } 105 if (!empty($link)) { 106 $content = '<a href="'.$link.'">'.$content.'</a>'; 107 } 108 109 return '<span class="' . $textClasses . '" onmouseover="autotooltip.show(event)" onmouseout="autotooltip.hide()" data-delay="' . $delay . '">' . 110 $content . 111 '<span class="plugin-autotooltip-hidden-classes">' . $classes . '</span>' . 112 '<!-- googleoff: all -->' . 113 '<span class="plugin-autotooltip-hidden-tip">' . 114 implode('<br><br>', $contentParts) . 115 '</span>' . 116 '<!-- googleon: all -->' . 117 '</span>'; 118 } 119 120 121 /** 122 * Render a tooltip, with the title and abstract of a page. 123 * 124 * @param string $id - A page id. 125 * @param string $content - The on-page content. Newlines will be rendered as line breaks. Omit to use the page's title. 126 * @param string $preTitle - Text to display before the title in the tooltip. Newlines will be rendered as line breaks. 127 * @param string $classes - CSS classes to add to this tooltip. 128 * @param string $textClasses - CSS classes to add to the linked text. 129 * @return string 130 */ 131 public function forWikilink($id, $content = null, $preTitle = '', $classes = '', $textClasses = '') { 132 global $ID; 133 //$id = resolve_id(getNS($ID), $id, false); 134 $resolver = new PageResolver($ID); 135 $id = $resolver->resolveId($id, null, true); 136 137 $meta = self::read_meta_fast($id); 138 $title = $meta['title']; 139 140 $link = $this->localRenderer->internallink($id, $content ?: $title, null, true); 141 142 if (page_exists(preg_replace('/\#.*$/', '', $id))) { 143 $link = $this->stripNativeTooltip($link); 144 return $this->forText($link, $meta['abstract'], $title, $preTitle, $classes, $textClasses); 145 } 146 else { 147 return $link; 148 } 149 } 150 151 152 /** 153 * Is this id excluded from the plugin? 154 * 155 * @param string $id 156 * @return boolean 157 */ 158 public function isExcluded($id) { 159 $inclusions = $this->getConf('linkall_inclusions'); 160 $exclusions = $this->getConf('linkall_exclusions'); 161 return (!empty($inclusions) && !preg_match("/$inclusions/", $id)) || 162 (!empty($exclusions) && preg_match("/$exclusions/", $id)); 163 } 164 165 166 /** 167 * Strip the native title= tooltip from an anchor tag. 168 * 169 * @param string $link 170 * @return string 171 */ 172 public function stripNativeTooltip($link) { 173 return preg_replace('/title="[^"]*"/', '', $link); 174 } 175 176 177 /** 178 * Reads specific metadata about 10x faster than p_get_metadata. p_get_metadata only uses caching for the current 179 * page, and uses the very slow php serialization. However, in a wiki with infrequently accessed pages, it's 180 * extremely slow. 181 * 182 * @param string $id 183 * @return array - An array containing 'title' and 'abstract.' 184 */ 185 static function read_meta_fast($id) { 186 global $ID; 187 188 $resolver = new PageResolver($ID); 189 $id = $resolver->resolveId(preg_replace('/\#.*$/', '', $id), null, true); 190 191 192 if (isset(self::$metaCache[$id])) { 193 return self::$metaCache[$id]; 194 } 195 196 $results = [ 197 'title' => p_get_metadata(cleanID($id), 'title'), 198 'abstract' => p_get_metadata(cleanID($id), 'plugin_description keywords') ?: p_get_metadata(cleanID($id), 'description abstract') 199 ]; 200 201 // By default, the abstract starts with the title. Remove it so it's not displayed twice, but still fetch 202 // both pieces of metadata, in case another plugin rewrote the abstract. 203 $results['abstract'] = preg_replace( 204 '/^' . self::_pregEscape($results['title']) . '(\r?\n)+/', 205 '', 206 $results['abstract'] 207 ); 208 209 self::$metaCache[$id] = $results; 210 return $results; 211 } 212 213 214 /** 215 * Format tooltip text. 216 * 217 * @param string $tt - Tooltip text. 218 * @return string 219 */ 220 private function _formatTT($tt) { 221 // Convert double-newlines into vertical space. 222 $tt = preg_replace('/(\r?\n){2,}/', '<br><br>', $tt); 223 // Single newlines get collapsed, just like in HTML. 224 return preg_replace('/(\r?\n)/', ' ', $tt); 225 } 226 227 228 /** 229 * Escape a string for inclusion in a regular expression, assuming forward slash is used as the delimiter. 230 * 231 * @param string $r - The regex string, without delimiters. 232 * @return string 233 */ 234 private static function _pregEscape($r) { 235 return preg_replace('/\//', '\\/', preg_quote($r)); 236 } 237} 238