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 work found in the deprecated publistf plugin 9 * and the bib2tpl (https://github.com/reitzig/bib2tpl) project and provides 10 * a fully compatible re-implementation of templating mechanisms. This includes 11 * special tokens, conditionals, and general parsing. 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 * Special tokens are available that give the template writer access to additional 30 * information: 31 * 32 * * `@globalcount@`: represents the total number of entries across all groups, 33 * note that entries can be repeated across multiple groups. 34 * * `@globalgroupcount@`: represents the total number of groups. 35 * * `@globalkey@`: unique key or label that can be used to template the same 36 * groups multiple times, but ensure that each set of groups 37 * can be handled separately. This is especially useful for 38 * HTML and Javascript interplay. 39 * * `@groupkey@`: unique key or label per group 40 * * `@groupcount@`: total number of entries in the group 41 * * `@groupid@`: unique id or index per group 42 * * `@entrykey@`: this is the Bibtex entry key 43 * 44 * **Note that tokens which exist in neither case are left untouched and appear in 45 * the output!** 46 */ 47class BB4DWTemplating 48{ 49 /** 50 * This function handles using 'templates' to specify the desired structure 51 * of the bib entries in DokuWiki. 52 * 53 * @param string $tpl The template given as a multiline string 54 * @param array $data Array containing *groups* and *config* values 55 * @return string The processed template with all tokens replaced 56 */ 57 public function process_template(string $tpl, array $data): string { 58 $groups = $data['groups']; 59 $result = $tpl; 60 61 // FIXME globalcount is currently not used 62 $result = preg_replace(['/@globalcount@/', '/@globalgroupcount@/', '/@globalkey@/'], 63 [0, count($groups), $data['config']['globalkey']], $result); 64 65 if ($data['config']['usegroup']) { 66 $pattern = '/@\{group@(.*?)@\}group@/s'; 67 $group_tpl = []; 68 preg_match($pattern, $result, $group_tpl); 69 70 while (!empty($group_tpl)) { 71 $groups_res = ''; 72 $id = 0; 73 74 foreach ($groups as $groupkey => $group) { 75 $groups_res .= $this->process_tpl_group($groupkey, $id++, $group, $group_tpl[1]); 76 } 77 78 $result = preg_replace($pattern, $groups_res, $result, 1); 79 preg_match($pattern, $result, $group_tpl); 80 } 81 } 82 83 return $result; 84 } 85 86 /** 87 * Process group-level template features 88 * 89 * @param string $groupkey The group name or keyword 90 * @param int $id The group numeric ID or index 91 * @param array $group Array containing the *entries* 92 * @param string $tpl The template to be processed 93 * @return string The processed template for the group 94 */ 95 private function process_tpl_group(string $groupkey, int $id, array $group, string $tpl): string { 96 $result = $tpl; 97 98 $result = preg_replace(['/@groupkey@/', '/@groupid@/', '/@groupcount@/'], 99 [$groupkey, $id, count($group)], 100 $result); 101 102 $pattern = '/@\{entry@(.*?)@\}entry@/s'; 103 104 // Extract entry templates 105 $entry_tpl = array(); 106 preg_match($pattern, $result, $entry_tpl); 107 108 // For all occurrences of an entry template 109 while (!empty($entry_tpl)) { 110 // Translate all entries 111 $entries_res = ''; 112 foreach ($group as $entryfields) { 113 $entries_res .= $this->process_tpl_entry($entryfields, $entry_tpl[1]); 114 } 115 116 $result = preg_replace($pattern, $entries_res, $result, 1); 117 preg_match($pattern, $result, $entry_tpl); 118 } 119 120 return $result; 121 } 122 123 /** 124 * Process entry-level template features 125 * 126 * @param BibEntry $entry A single *entry* 127 * @param string $tpl The template to be processed 128 * @return string The processed template for the group 129 */ 130 private function process_tpl_entry(array $entryfields, string $tpl): string { 131 $result = $tpl; 132 133 // XXX bib2tpl template uses `entrykey` for Bibtex `key`, here we manually 134 // replace this rather than adding an additional field into the entry array. 135 $result = preg_replace(['/@entrykey@/'], 136 [$entryfields['key']], 137 $result); 138 139 // Resolve all conditions 140 $result = $this->resolve_conditions($entryfields, $result); 141 142 // Replace all possible unconditional fields 143 $patterns = []; 144 $replacements = []; 145 146 foreach ($entryfields as $key => $value) { 147 if ($key === 'author') { 148 $value = $entryfields['niceauthor']; 149 } 150 $patterns[] = '/@'.$key.'@/'; 151 $replacements[] = $value; 152 } 153 154 return preg_replace($patterns, $replacements, $result); 155 } 156 157 /** 158 * This function eliminates conditions in template parts. 159 * 160 * It is almost exact copy of the function `resolve_conditions` from 161 * `bib2tpl/bibtex_converter.php`. 162 * 163 * @param array entry Entry with respect to which conditions are to be 164 * solved. 165 * @param string template The entry part of the template. 166 * @return string Template string without conditions. 167 */ 168 private function resolve_conditions(array $entry, string &$string): string { 169 $pattern = '/@\?(\w+)(?:(<=|>=|==|!=|~)(.*?))?@(.*?)(?:@:\1@(.*?))?@;\1@/s'; 170 /* There are two possibilities for mode: existential or value check 171 * Then, there can be an else part or not. 172 * Existential Value Check RegExp 173 * Group 1 field field \w+ 174 * Group 2 then operator .*? / <=|>=|==|!=|~ 175 * Group 3 [else] value .*? 176 * Group 4 --- then .*? 177 * Group 5 --- [else] .*? 178 */ 179 180 $match = []; 181 182 /* Would like to do 183 * preg_match_all($pattern, $string, $matches); 184 * to get all matches at once but that results in Segmentation 185 * fault. Therefore iteratively: 186 */ 187 while (preg_match($pattern, $string, $match)) { 188 $resolved = ''; 189 190 $evalcond = !empty($entry[$match[1]]); 191 $then = count($match) > 3 ? 4 : 2; 192 $else = count($match) > 3 ? 5 : 3; 193 194 if ( $evalcond && count($match) > 3 ) { 195 if ( $match[2] === '==' ) { 196 $evalcond = $entry[$match[1]] === $match[3]; 197 } 198 elseif ( $match[2] === '!=' ) { 199 $evalcond = $entry[$match[1]] !== $match[3]; 200 } 201 elseif ( $match[2] === '<=' ) { 202 $evalcond = is_numeric($entry[$match[1]]) 203 && is_numeric($match[3]) 204 && (int)$entry[$match[1]] <= (int)$match[3]; 205 } 206 elseif ( $match[2] === '>=' ) { 207 $evalcond = is_numeric($entry[$match[1]]) 208 && is_numeric($match[3]) 209 && (int)$entry[$match[1]] >= (int)$match[3]; 210 } 211 elseif ( $match[2] === '~' ) { 212 $evalcond = preg_match('/'.$match[3].'/', $entry[$match[1]]) > 0; 213 } 214 } 215 216 if ( $evalcond ) 217 { 218 $resolved = $match[$then]; 219 } 220 elseif ( !empty($match[$else]) ) 221 { 222 $resolved = $match[$else]; 223 } 224 225 // Recurse to cope with nested conditions 226 $resolved = $this->resolve_conditions($entry, $resolved); 227 228 $string = str_replace($match[0], $resolved, $string); 229 } 230 231 return $string; 232 } 233 234} 235 236?> 237