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	 * @return string
66	 */
67	public function forText($content, $tooltip, $title='', $preTitle = '', $classes = '', $textClasses = '') {
68		if (empty($classes)) {
69			$classes = $this->getConf('style');
70		}
71		if (empty($classes)) {
72			$classes = 'default';
73		}
74		$delay = $this->getConf('delay') ?: 0;
75
76		// Sanitize
77		$classes = htmlspecialchars($classes);
78		// Add the plugin prefix to all classes.
79		$classes = preg_replace('/(\w+)/', 'plugin-autotooltip__$1', $classes);
80
81		$partCount = (empty($title) ? 0 : 1) + (empty($preTitle) ? 0 : 1) + (empty($tooltip) ? 0 : 1);
82		if ($partCount > 1 || strchr($tooltip, "\n") !== FALSE || strlen($tooltip) > 40) {
83			$classes .= ' plugin-autotooltip_big';
84		}
85
86		if (empty($textClasses)) {
87			$textClasses = 'plugin-autotooltip_linked';
88			if (strstr($content, '<a ') === FALSE) {
89				$textClasses .= ' 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="' . $textClasses . '" onmouseover="autotooltip.show(event)" onmouseout="autotooltip.hide()" data-delay="' . $delay . '">' .
105			$content .
106			'<span class="plugin-autotooltip-hidden-classes">' . $classes . '</span>' .
107			'<!-- googleoff: all -->' .
108			'<span class="plugin-autotooltip-hidden-tip">' .
109			implode('<br><br>', $contentParts) .
110			'</span>' .
111			'<!-- googleon: all -->' .
112		'</span>';
113	}
114
115
116	/**
117	 * Render a tooltip, with the title and abstract of a page.
118	 *
119	 * @param string $id - A page id.
120	 * @param string $content - The on-page content. Newlines will be rendered as line breaks. Omit to use the page's title.
121	 * @param string $preTitle - Text to display before the title in the tooltip. Newlines will be rendered as line breaks.
122	 * @param string $classes - CSS classes to add to this tooltip.
123	 * @param string $textClasses - CSS classes to add to the linked text.
124	 * @return string
125	 */
126	public function forWikilink($id, $content = null, $preTitle = '', $classes = '', $textClasses = '') {
127		global $ID;
128		$id = resolve_id(getNS($ID), $id, false);
129
130		$meta = self::read_meta_fast($id);
131		$title = $meta['title'];
132
133		$link = $this->localRenderer->internallink($id, $content ?: $title, null, true);
134
135		if (page_exists(preg_replace('/\#.*$/', '', $id))) {
136			$link = $this->stripNativeTooltip($link);
137			return $this->forText($link, $meta['abstract'], $title, $preTitle, $classes, $textClasses);
138		}
139		else {
140			return $link;
141		}
142	}
143
144
145	/**
146	 * Is this id excluded from the plugin?
147	 *
148	 * @param string $id
149	 * @return boolean
150	 */
151	public function isExcluded($id) {
152		$inclusions = $this->getConf('linkall_inclusions');
153		$exclusions = $this->getConf('linkall_exclusions');
154		return (!empty($inclusions) && !preg_match("/$inclusions/", $id)) ||
155			(!empty($exclusions) && preg_match("/$exclusions/", $id));
156	}
157
158
159	/**
160	 * Strip the native title= tooltip from an anchor tag.
161	 *
162	 * @param string $link
163	 * @return string
164	 */
165	public function stripNativeTooltip($link) {
166		return preg_replace('/title="[^"]*"/', '', $link);
167	}
168
169
170	/**
171	 * Reads specific metadata about 10x faster than p_get_metadata. p_get_metadata only uses caching for the current
172	 * page, and uses the very slow php serialization. However, in a wiki with infrequently accessed pages, it's
173	 * extremely slow.
174	 *
175	 * @param string $id
176	 * @return array - An array containing 'title' and 'abstract.'
177	 */
178	static function read_meta_fast($id) {
179		global $ID;
180
181		$resolver = new PageResolver($ID);
182		$id = $resolver->resolveId(preg_replace('/\#.*$/', '', $id), null, true);
183
184
185		if (isset(self::$metaCache[$id])) {
186			return self::$metaCache[$id];
187		}
188
189		$results = [
190			'title' => p_get_metadata(cleanID($id), 'title'),
191			'abstract' => p_get_metadata(cleanID($id), 'plugin_description keywords') ?: p_get_metadata(cleanID($id), 'description abstract')
192		];
193
194		// By default, the abstract starts with the title. Remove it so it's not displayed twice, but still fetch
195		// both pieces of metadata, in case another plugin rewrote the abstract.
196		$results['abstract'] = preg_replace(
197			'/^' . self::_pregEscape($results['title']) . '(\r?\n)+/',
198			'',
199			$results['abstract']
200		);
201
202		self::$metaCache[$id] = $results;
203		return $results;
204	}
205
206
207	/**
208	 * Format tooltip text.
209	 *
210	 * @param string $tt - Tooltip text.
211	 * @return string
212	 */
213	private function _formatTT($tt) {
214		// Convert double-newlines into vertical space.
215		$tt = preg_replace('/(\r?\n){2,}/', '<br><br>', $tt);
216		// Single newlines get collapsed, just like in HTML.
217		return preg_replace('/(\r?\n)/', ' ', $tt);
218	}
219
220
221	/**
222	 * Escape a string for inclusion in a regular expression, assuming forward slash is used as the delimiter.
223	 *
224	 * @param string $r - The regex string, without delimiters.
225	 * @return string
226	 */
227	private static function _pregEscape($r) {
228		return preg_replace('/\//', '\\/', preg_quote($r));
229	}
230}
231