1<?php
2/**
3 * AutoLink 4 DokuWiki plugin
4 *
5 * @license    MIT
6 * @author     Eli Fenton
7 */
8if(!defined('DOKU_INC')) die();
9require_once(DOKU_PLUGIN.'autolink4/consts.php');
10
11class helper_plugin_autolink4 extends DokuWiki_Plugin {
12	use autotooltip4_consts;
13	const CONFIG_FILE = DOKU_CONF . 'autolink4.conf';
14
15	static $didInit = false;
16	static $subs = [];
17	static $regexSubs = []; // flat array of data
18	static $simpleSubs = []; // 2d map of [namespace][string match]=>data
19
20	private $ignoreMatches = []; // Ignore these, because they were already found once and they're configured to be unique.
21
22	public function getSubs() {
23		return self::$subs; //TODO: Remove this later?
24	}
25
26	/**
27	 * Saves the config file
28	 *
29	 * @param string $config the raw text for the config
30	 * @return bool
31	 */
32	public function saveConfigFile($config) {
33		return io_saveFile(self::CONFIG_FILE, cleanText($config));
34	}
35
36
37	/**
38	 * Load the config file
39	 */
40	public function loadConfigFile() {
41		if (file_exists(self::CONFIG_FILE)) {
42			return io_readFile(self::CONFIG_FILE);
43		}
44	}
45
46
47	/**
48	 * Load the config file
49	 */
50	public function loadAndProcessConfigFile() {
51		// Only load once, so we don't re-process with things like plugin:include.
52		if (self::$didInit) {
53			return;
54		}
55		self::$didInit = true;
56
57		if (!file_exists(self::CONFIG_FILE)) {
58			return;
59		}
60
61		$cfg = io_readFile(self::CONFIG_FILE);
62
63		global $ID;
64		$current_ns = getNS($ID);
65
66		// Convert the config into usable data.
67		$lines = preg_split('/[\n\r]+/', $cfg);
68		foreach ($lines as $line) {
69			$line = trim($line);
70			if (strlen($line) == 0) {
71				continue;
72			}
73
74			$data = array_pad(str_getcsv($line), self::$MAX_VAL, '');
75			if (!strlen($data[self::$ORIG]) || !strlen($data[self::$TO])) {
76				continue;
77			}
78
79			$orig = trim($data[self::$ORIG]);
80
81			$ns = isset($data[self::$IN]) ? trim($data[self::$IN]) : null;
82			if (!$this->inNS($current_ns, $ns)) {
83				continue;
84			}
85
86			$s = [];
87			$s[self::$ORIG] = $orig;
88			$s[self::$TO] = trim($data[self::$TO]);
89			$s[self::$IN] = $ns;
90			$s[self::$FLAGS] = isset($data[self::$FLAGS]) ? trim($data[self::$FLAGS]) : null;
91			$s[self::$TOOLTIP] = isset($data[self::$FLAGS]) ? strstr($data[self::$FLAGS], 'tt') !== FALSE : false;
92			$s[self::$ONCE] = isset($data[self::$FLAGS]) ? strstr($data[self::$FLAGS], 'once') !== FALSE : false;
93			$s[self::$INWORD] = isset($data[self::$FLAGS]) ? strstr($data[self::$FLAGS], 'inword') !== FALSE : false;
94
95			// Add word breaks, and collapse one space (allows newlines).
96			if ($s[self::$INWORD]) {
97				$s[self::$MATCH] = preg_replace('/ /', '\s', $orig);
98			}
99			else {
100				$s[self::$MATCH] = '\b' . preg_replace('/ /', '\s', $orig) . '\b';
101			}
102
103			self::$subs[] = $s;
104
105			if (preg_match('/[\\\[?.+*^$]/', $orig)) {
106				self::$regexSubs[] = $s;
107			}
108			else {
109				// If the search string is not a regex, cache it right away, so we don't have to loop
110				// through regexes later.
111				$s = $this->cacheMatch($orig, $s);
112			}
113		}
114	}
115
116
117        /**
118         * Get match data from a string.
119         */
120	public function getMatch($match) {
121                // If there's a matching non-regex pattern, or we cached it after finding the regex patter on the page,
122                // we can load it from the cache.
123                $found = self::$simpleSubs[$match] ?? null;
124		if ($found != null) {
125			return $found;
126		}
127
128		// There's no way to determine which match sent us here, so we have to loop through the whole list.
129		foreach (self::$regexSubs as &$s) {
130			if (preg_match('/^' . $s[self::$MATCH] . '$/', $match)) {
131				// Cache the matched string, so we don't have to loop more than once for the same match.
132				$found = $this->cacheMatch($match, $s);
133				break;
134			}
135		}
136		return $found;
137	}
138
139	/**
140	 * Call this in your xhtml renderer code to decide whether it should be rendered as plain text.
141	 *
142	 * @param Object $data - The return value of getMatch().
143	 */
144	public function shouldRenderPlainText($data) {
145		if (is_string($data)) {
146			return true;
147		}
148		$text = $data[self::$TEXT];
149		$match = $data[self::$MATCH] ?? $text;
150
151		if (array_key_exists($text, $this->ignoreMatches) || array_key_exists($match, $this->ignoreMatches)) {
152                       	return true;
153               	}
154
155		if ($data[self::$ONCE]) {
156                        $this->ignoreMatches[$text] = true;
157			$this->ignoreMatches[$match] = true;
158                }
159		return false;
160	}
161
162
163        /**
164         * Cache a simple match
165         */
166	public function cacheMatch($match, $data) {
167		// We usually call this with a different text match, so that two things can link to the same page.
168		$data[self::$TEXT] = $match;
169		self::$simpleSubs[$match] = $data;
170		return $data;
171	}
172
173	/**
174	 * Is one namespace inside another.
175	 *
176	 * @param string $contained - Is this namespace...
177	 * @param string $container - Equal to or under this one.
178	 * @return bool
179	 */
180	function inNS(string $contained, ?string $container) {
181                if ($container == null || strlen($container) === 0) {
182                        return true;
183               	}
184
185                $testA = explode(':', $container);
186                $contained = implode(':', array_slice(explode(':', $contained), 0, count($testA)));
187                return $container == $contained;
188	}
189}
190
191/*
192==Unit tests to plug into a PHP sandbox (yeah, I should make these real unit tests)==
193
194function test(string $ns, ?string $test, bool $expected) {
195	if (inNS($ns, $test) != $expected) {
196		echo "Fail: inNS($ns, $test) should be ".($expected?'true':'false')."\n";
197	}
198}
199$tests = [
200	['abc', null, true],
201	['abc', 'abd', false],
202	['abc:d:e', 'abc:d', true],
203	['abc:de', 'abc:d', false],
204	['abc', 'abc:d', false],
205];
206foreach ($tests as $t) {
207	test($t[0], $t[1], $t[2]);
208}
209*/
210