1<?php 2/** 3 * DokuWiki Plugin bb4dw (Templating Component) 4 * 5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 6 * @author Hans-Nikolai Viessmann <hans@viess.mn> 7 * 8 * This class is based upon the wirking in the deprecated publistf plugin 9 * and the bib2tpl (https://github.com/reitzig/bib2tpl) project. We make use 10 * of the exactly the same templating mechanism, specifically how conditionals 11 * are structured and parsed. 12 * 13 * At its core, this class resolves a template of the format: 14 * 15 * ``` 16 * @{group@ 17 * === @groupkey@ === 18 * @{entry@ 19 * # further tokens like 20 * @key@ @title@ @author@ 21 * @}entry@ 22 * @}group@ 23 * ``` 24 * 25 * where a *group* contains multiple *entries*. All tokens are encoded using `@` 26 * (at) symbols, these either (i) refer to context specific values, or (ii) to 27 * values stored within the entry, in our case Bibtex. 28 * 29 * **Note that tokens which exist in neither case are left untouched and appear in 30 * the output!** 31 */ 32class BB4DWTemplating 33{ 34 /** 35 * This function handles using 'templates' to specify the desired structure 36 * of the bib entries in DokuWiki. 37 * 38 * @param string $tpl The template given as a multiline string 39 * @param array $data Array containing *groups* and *config* values 40 * @return string The processed template with all tokens replaced 41 */ 42 function process_template(string $tpl, array $data): string { 43 $groups = $data['groups']; 44 $result = $tpl; 45 46 // FIXME globalcount is currently not used 47 $result = preg_replace(['/@globalcount@/', '/@globalgroupcount@/', '/@globalkey@/'], 48 [0, count($groups), $data['config']['globalkey']], $result); 49 50 if ($data['config']['usegroup']) { 51 $pattern = '/@\{group@(.*?)@\}group@/s'; 52 $group_tpl = []; 53 preg_match($pattern, $result, $group_tpl); 54 55 while (!empty($group_tpl)) { 56 $groups_res = ''; 57 $id = 0; 58 59 foreach ($groups as $groupkey => $group) { 60 $groups_res .= $this->process_tpl_group($groupkey, $id++, $group, $group_tpl[1]); 61 } 62 63 $result = preg_replace($pattern, $groups_res, $result, 1); 64 preg_match($pattern, $result, $group_tpl); 65 } 66 } 67 68 return $result; 69 } 70 71 /** 72 * Process group-level template features 73 * 74 * @param string $groupkey The group name or keyword 75 * @param int $id The group numeric ID or index 76 * @param array $group Array containing the *entries* 77 * @param string $tpl The template to be processed 78 * @return string The processed template for the group 79 */ 80 private function process_tpl_group(string $groupkey, int $id, array $group, string $tpl): string { 81 $result = $tpl; 82 83 //if ( $this->options['group'] === 'entrytype' ) { 84 // $key = $this->options['lang']['entrytypes'][$key]; 85 //} 86 $result = preg_replace(['/@groupkey@/', '/@groupid@/', '/@groupcount@/'], 87 [$groupkey, $id, count($group)], 88 $result); 89 90 $pattern = '/@\{entry@(.*?)@\}entry@/s'; 91 92 // Extract entry templates 93 $entry_tpl = array(); 94 preg_match($pattern, $result, $entry_tpl); 95 96 // For all occurrences of an entry template 97 while ( !empty($entry_tpl) ) { 98 // Translate all entries 99 $entries_res = ''; 100 foreach ($group as $entryfields) { 101 $entries_res .= $this->process_tpl_entry($entryfields, $entry_tpl[1]); 102 } 103 104 $result = preg_replace($pattern, $entries_res, $result, 1); 105 preg_match($pattern, $result, $entry_tpl); 106 } 107 108 return $result; 109 } 110 111 /** 112 * Process entry-level template features 113 * 114 * @param BibEntry $entry A single *entry* 115 * @param string $tpl The template to be processed 116 * @return string The processed template for the group 117 */ 118 private function process_tpl_entry(array $entryfields, string $tpl): string { 119 $result = $tpl; 120 121 // XXX bib2tpl template uses `entrykey` for Bibtex `key`, here we manually 122 // replace this rather than adding an additional field into the entry array. 123 $result = preg_replace(['/@entrykey@/'], 124 [$entryfields['key']], 125 $result); 126 127 // Resolve all conditions 128 $result = $this->resolve_conditions($entryfields, $result); 129 130 // Replace all possible unconditional fields 131 $patterns = []; 132 $replacements = []; 133 134 foreach ($entryfields as $key => $value) { 135 if ($key === 'author') { 136 $value = $entryfields['niceauthor']; 137 } 138 $patterns[] = '/@'.$key.'@/'; 139 $replacements[] = $value; 140 } 141 142 return preg_replace($patterns, $replacements, $result); 143 } 144 145 /** 146 * This function eliminates conditions in template parts. 147 * 148 * @param array entry Entry with respect to which conditions are to be 149 * solved. 150 * @param string template The entry part of the template. 151 * @return string Template string without conditions. 152 */ 153 private function resolve_conditions(array $entry, string &$string): string { 154 $pattern = '/@\?(\w+)(?:(<=|>=|==|!=|~)(.*?))?@(.*?)(?:@:\1@(.*?))?@;\1@/s'; 155 /* There are two possibilities for mode: existential or value check 156 * Then, there can be an else part or not. 157 * Existential Value Check RegExp 158 * Group 1 field field \w+ 159 * Group 2 then operator .*? / <=|>=|==|!=|~ 160 * Group 3 [else] value .*? 161 * Group 4 --- then .*? 162 * Group 5 --- [else] .*? 163 */ 164 165 $match = []; 166 167 /* Would like to do 168 * preg_match_all($pattern, $string, $matches); 169 * to get all matches at once but that results in Segmentation 170 * fault. Therefore iteratively: 171 */ 172 while (preg_match($pattern, $string, $match)) { 173 $resolved = ''; 174 175 $evalcond = !empty($entry[$match[1]]); 176 $then = count($match) > 3 ? 4 : 2; 177 $else = count($match) > 3 ? 5 : 3; 178 179 if ( $evalcond && count($match) > 3 ) { 180 if ( $match[2] === '==' ) { 181 $evalcond = $entry[$match[1]] === $match[3]; 182 } 183 elseif ( $match[2] === '!=' ) { 184 $evalcond = $entry[$match[1]] !== $match[3]; 185 } 186 elseif ( $match[2] === '<=' ) { 187 $evalcond = is_numeric($entry[$match[1]]) 188 && is_numeric($match[3]) 189 && (int)$entry[$match[1]] <= (int)$match[3]; 190 } 191 elseif ( $match[2] === '>=' ) { 192 $evalcond = is_numeric($entry[$match[1]]) 193 && is_numeric($match[3]) 194 && (int)$entry[$match[1]] >= (int)$match[3]; 195 } 196 elseif ( $match[2] === '~' ) { 197 $evalcond = preg_match('/'.$match[3].'/', $entry[$match[1]]) > 0; 198 } 199 } 200 201 if ( $evalcond ) 202 { 203 $resolved = $match[$then]; 204 } 205 elseif ( !empty($match[$else]) ) 206 { 207 $resolved = $match[$else]; 208 } 209 210 // Recurse to cope with nested conditions 211 $resolved = $this->resolve_conditions($entry, $resolved); 212 213 $string = str_replace($match[0], $resolved, $string); 214 } 215 216 return $string; 217 } 218 219} 220 221?> 222