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