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