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