xref: /plugin/autotooltip/helper.php (revision d39942acf22d9f01780a7916bedd6c84062f0cb1)
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