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 a simple match
119         */
120	public function getMatch($match) {
121               	if (array_key_exists($match, $this->ignoreMatches)) {
122                       	return null;
123                }
124
125                // If there's a matching non-regex pattern, or we cached it after finding the regex patter on the page,
126                // we can load it from the cache.
127		$found = null;
128                if (isset(self::$simpleSubs[$match])) {
129                        $found = self::$simpleSubs[$match];
130                }
131
132		if ($found == null) {
133			// There's no way to determine which match sent us here, so we have to loop through the whole list.
134			foreach (self::$regexSubs as &$s) {
135				if (preg_match('/^' . $s[self::$MATCH] . '$/', $match)) {
136					// Cache the matched string, so we don't have to loop more than once for the same match.
137					$found = $this->cacheMatch($match, $s);
138					break;
139				}
140			}
141		}
142
143		if ($found != null && $found[self::$ONCE]) {
144			$this->ignoreMatches[$match] = true;
145		}
146		return $found;
147	}
148
149
150        /**
151         * Cache a simple match
152         */
153	public function cacheMatch($match, $data) {
154		// We usually call this with a different text match, so that two things can link to the same page.
155		$data[self::$TEXT] = $match;
156		self::$simpleSubs[$match] = $data;
157		return $data;
158	}
159
160	/**
161	 * Is one namespace inside another.
162	 *
163	 * @param string $ns - Search inside this namespace.
164	 * @param string $test - Look for this namespace.
165	 * @return bool
166	 */
167	function inNS($ns, $test) {
168		$len = strlen($test);
169		return !$len || substr($ns, 0, $len) == $test;
170	}
171}
172