1<?php 2 3use dokuwiki\ErrorHandler; 4use dokuwiki\plugin\struct\meta\SearchConfig; 5use dokuwiki\plugin\struct\meta\StructException; 6 7/** 8 * DokuWiki Plugin structautolink (Renderer Component) 9 * 10 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 11 * @author Andreas Gohr <gohr@cosmocode.de> 12 */ 13class renderer_plugin_structautolink extends Doku_Renderer_xhtml 14{ 15 /** @var array[] The glossary terms per page */ 16 protected $glossary = []; 17 18 /** @var string The compound regex to match all terms */ 19 protected $regex; 20 21 // region renderer methods 22 23 /** 24 * Make this renderer available as alternative default renderer 25 * 26 * @param string $format 27 * @return bool 28 */ 29 public function canRender($format) 30 { 31 if ($format == 'xhtml') return true; 32 return false; 33 } 34 35 /** @inheritdoc */ 36 public function document_start() 37 { 38 parent::document_start(); 39 $this->setGlossary($this->loadGlossary()); 40 } 41 42 /** @inheritDoc */ 43 public function cdata($text) 44 { 45 global $ID; 46 47 // only auto-link if wanted 48 if ($this->getConf('match') && !preg_match('/' . $this->getConf('match') . '/i', ":$ID")) { 49 parent::cdata($text); 50 return; 51 } 52 53 $tokens = $this->findMatchingTokens($text); 54 if (!$tokens) { 55 parent::cdata($text); 56 return; 57 } 58 59 $start = 0; 60 foreach ($tokens as $token) { 61 if ($token['pos'] > $start) { 62 parent::cdata(substr($text, $start, $token['pos'] - $start)); 63 } 64 $this->internallink($this->getConf('ns') . ':' . $token['id'], $token['term']); 65 $start = $token['pos'] + $token['len']; 66 } 67 if ($start < strlen($text)) { 68 parent::cdata(substr($text, $start)); 69 } 70 71 } 72 73 // endregion 74 // region logic methods 75 76 /** 77 * Load the defined glossary terms from struct 78 * 79 * @return array[] [pageid => [terms, ...], ...] 80 */ 81 public function loadGlossary() 82 { 83 $schema = $this->getConf('schema'); 84 $field = $this->getConf('field'); 85 if (!$schema || !$field) return []; 86 87 try { 88 $search = new SearchConfig([ 89 'schemas' => [[$schema, 'glossary']], 90 'cols' => ['%pageid%', $field], 91 ]); 92 $data = $search->execute(); 93 } catch (StructException $e) { 94 ErrorHandler::logException($e); 95 return []; 96 } 97 98 $glossary = []; 99 foreach ($data as $row) { 100 $glossary[$row[0]->getValue()] = $row[1]->getValue(); 101 } 102 103 return $glossary; 104 } 105 106 /** 107 * Set the given glossary and rebuild the regex 108 * 109 * @param array[] $glossary [pageid => [terms, ...], ...] 110 */ 111 public function setGlossary($glossary) 112 { 113 $this->glossary = $glossary; 114 $this->buildPatterns(); 115 } 116 117 118 /** 119 * initializes the regex to match terms 120 */ 121 public function buildPatterns() 122 { 123 if (!$this->glossary) { 124 $this->regex = null; 125 return; 126 } 127 128 $patterns = []; 129 $num = 0; // term number 130 foreach ($this->glossary as $terms) { 131 $terms = array_map('preg_quote_cb', $terms); 132 $patterns[] = '(?P<p' . ($num++) . '>' . join('|', $terms) . ')'; 133 } 134 135 $this->regex = '/\b(?:' . implode('|', $patterns) . ')\b/'; 136 } 137 138 /** 139 * Find all matching glossary tokens in the given text 140 * 141 * @param string $text 142 * @return array|false Either an array of tokens or false if no matches were found 143 */ 144 public function findMatchingTokens($text) 145 { 146 global $ID; 147 148 if (!$this->regex) return false; 149 150 if (!preg_match_all($this->regex, $text, $matches, PREG_OFFSET_CAPTURE)) { 151 return false; 152 } 153 154 $tokens = []; 155 foreach (array_keys($this->glossary) as $num => $id) { 156 if (!$this->glossary[$id]) continue; // this page has been linked before 157 if ($id === $ID) continue; // don't link to the current page 158 159 foreach ($matches["p$num"] as $match) { 160 if ($match[0] === '') continue; 161 $tokens[] = [ 162 'id' => $id, 163 'term' => $match[0], 164 'pos' => $match[1], 165 'len' => strlen($match[0]), 166 ]; 167 $this->glossary[$id] = false; // don't link this page again 168 break; // don't link any other term of this page 169 } 170 } 171 172 // sort by position 173 usort($tokens, function ($a, $b) { 174 return $a['pos'] - $b['pos']; 175 }); 176 177 return $tokens; 178 } 179 180 181} 182 183