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		$resolver = new PageResolver($ID);
130                $id = $resolver->resolveId($id, null, true);
131
132		$meta = self::read_meta_fast($id);
133		$title = $meta['title'];
134
135		$link = $this->localRenderer->internallink($id, $content ?: $title, null, true);
136
137		if (page_exists(preg_replace('/\#.*$/', '', $id))) {
138			$link = $this->stripNativeTooltip($link);
139			return $this->forText($link, $meta['abstract'], $title, $preTitle, $classes, $textClasses);
140		}
141		else {
142			return $link;
143		}
144	}
145
146
147	/**
148	 * Is this id excluded from the plugin?
149	 *
150	 * @param string $id
151	 * @return boolean
152	 */
153	public function isExcluded($id) {
154		$inclusions = $this->getConf('linkall_inclusions');
155		$exclusions = $this->getConf('linkall_exclusions');
156		return (!empty($inclusions) && !preg_match("/$inclusions/", $id)) ||
157			(!empty($exclusions) && preg_match("/$exclusions/", $id));
158	}
159
160
161	/**
162	 * Strip the native title= tooltip from an anchor tag.
163	 *
164	 * @param string $link
165	 * @return string
166	 */
167	public function stripNativeTooltip($link) {
168		return preg_replace('/title="[^"]*"/', '', $link);
169	}
170
171
172	/**
173	 * Reads specific metadata about 10x faster than p_get_metadata. p_get_metadata only uses caching for the current
174	 * page, and uses the very slow php serialization. However, in a wiki with infrequently accessed pages, it's
175	 * extremely slow.
176	 *
177	 * @param string $id
178	 * @return array - An array containing 'title' and 'abstract.'
179	 */
180	static function read_meta_fast($id) {
181		global $ID;
182
183		$resolver = new PageResolver($ID);
184		$id = $resolver->resolveId(preg_replace('/\#.*$/', '', $id), null, true);
185
186
187		if (isset(self::$metaCache[$id])) {
188			return self::$metaCache[$id];
189		}
190
191		$results = [
192			'title' => p_get_metadata(cleanID($id), 'title'),
193			'abstract' => p_get_metadata(cleanID($id), 'plugin_description keywords') ?: p_get_metadata(cleanID($id), 'description abstract')
194		];
195
196		// By default, the abstract starts with the title. Remove it so it's not displayed twice, but still fetch
197		// both pieces of metadata, in case another plugin rewrote the abstract.
198		$results['abstract'] = preg_replace(
199			'/^' . self::_pregEscape($results['title']) . '(\r?\n)+/',
200			'',
201			$results['abstract']
202		);
203
204		self::$metaCache[$id] = $results;
205		return $results;
206	}
207
208
209	/**
210	 * Format tooltip text.
211	 *
212	 * @param string $tt - Tooltip text.
213	 * @return string
214	 */
215	private function _formatTT($tt) {
216		// Convert double-newlines into vertical space.
217		$tt = preg_replace('/(\r?\n){2,}/', '<br><br>', $tt);
218		// Single newlines get collapsed, just like in HTML.
219		return preg_replace('/(\r?\n)/', ' ', $tt);
220	}
221
222
223	/**
224	 * Escape a string for inclusion in a regular expression, assuming forward slash is used as the delimiter.
225	 *
226	 * @param string $r - The regex string, without delimiters.
227	 * @return string
228	 */
229	private static function _pregEscape($r) {
230		return preg_replace('/\//', '\\/', preg_quote($r));
231	}
232}
233