* * This class is based upon the work found in the deprecated publistf plugin * and the bib2tpl (https://github.com/reitzig/bib2tpl) project and provides * a fully compatible re-implementation of templating mechanisms. This includes * special tokens, conditionals, and general parsing. * * At its core, this class resolves a template of the format: * * ``` * @{group@ * === @groupkey@ === * @{entry@ * # further tokens like * @key@ @title@ @author@ * @}entry@ * @}group@ * ``` * * where a *group* contains multiple *entries*. All tokens are encoded using `@` * (at) symbols, these either (i) refer to context specific values, or (ii) to * values stored within the entry, in our case Bibtex. * * Special tokens are available that give the template writer access to additional * information: * * * `@globalcount@`: represents the total number of entries across all groups, * note that entries can be repeated across multiple groups. * * `@globalgroupcount@`: represents the total number of groups. * * `@globalkey@`: unique key or label that can be used to template the same * groups multiple times, but ensure that each set of groups * can be handled separately. This is especially useful for * HTML and Javascript interplay. * * `@groupkey@`: unique key or label per group * * `@groupcount@`: total number of entries in the group * * `@groupid@`: unique id or index per group * * `@entrykey@`: this is the Bibtex entry key * * **Note that tokens which exist in neither case are left untouched and appear in * the output!** */ class BB4DWTemplating { /** * This function handles using 'templates' to specify the desired structure * of the bib entries in DokuWiki. * * @param string $tpl The template given as a multiline string * @param array $data Array containing *groups* and *config* values * @return string The processed template with all tokens replaced */ public function process_template(string $tpl, array $data): string { $groups = $data['groups']; $result = $tpl; // FIXME globalcount is currently not used $result = preg_replace(['/@globalcount@/', '/@globalgroupcount@/', '/@globalkey@/'], [0, count($groups), $data['config']['globalkey']], $result); if ($data['config']['usegroup']) { $pattern = '/@\{group@(.*?)@\}group@/s'; $group_tpl = []; preg_match($pattern, $result, $group_tpl); while (!empty($group_tpl)) { $groups_res = ''; $id = 0; foreach ($groups as $groupkey => $group) { $groups_res .= $this->process_tpl_group($groupkey, $id++, $group, $group_tpl[1]); } $result = preg_replace($pattern, $groups_res, $result, 1); preg_match($pattern, $result, $group_tpl); } } return $result; } /** * Process group-level template features * * @param string $groupkey The group name or keyword * @param int $id The group numeric ID or index * @param array $group Array containing the *entries* * @param string $tpl The template to be processed * @return string The processed template for the group */ private function process_tpl_group(string $groupkey, int $id, array $group, string $tpl): string { $result = $tpl; $result = preg_replace(['/@groupkey@/', '/@groupid@/', '/@groupcount@/'], [$groupkey, $id, count($group)], $result); $pattern = '/@\{entry@(.*?)@\}entry@/s'; // Extract entry templates $entry_tpl = array(); preg_match($pattern, $result, $entry_tpl); // For all occurrences of an entry template while (!empty($entry_tpl)) { // Translate all entries $entries_res = ''; foreach ($group as $entryfields) { $entries_res .= $this->process_tpl_entry($entryfields, $entry_tpl[1]); } $result = preg_replace($pattern, $entries_res, $result, 1); preg_match($pattern, $result, $entry_tpl); } return $result; } /** * Process entry-level template features * * @param BibEntry $entry A single *entry* * @param string $tpl The template to be processed * @return string The processed template for the group */ private function process_tpl_entry(array $entryfields, string $tpl): string { $result = $tpl; // XXX bib2tpl template uses `entrykey` for Bibtex `key`, here we manually // replace this rather than adding an additional field into the entry array. $result = preg_replace(['/@entrykey@/'], [$entryfields['key']], $result); // Resolve all conditions $result = $this->resolve_conditions($entryfields, $result); // Replace all possible unconditional fields $patterns = []; $replacements = []; foreach ($entryfields as $key => $value) { if ($key === 'author') { $value = $entryfields['niceauthor']; } $patterns[] = '/@'.$key.'@/'; $replacements[] = $value; } return preg_replace($patterns, $replacements, $result); } /** * This function eliminates conditions in template parts. * * It is almost exact copy of the function `resolve_conditions` from * `bib2tpl/bibtex_converter.php`. * * @param array entry Entry with respect to which conditions are to be * solved. * @param string template The entry part of the template. * @return string Template string without conditions. */ private function resolve_conditions(array $entry, string &$string): string { $pattern = '/@\?(\w+)(?:(<=|>=|==|!=|~)(.*?))?@(.*?)(?:@:\1@(.*?))?@;\1@/s'; /* There are two possibilities for mode: existential or value check * Then, there can be an else part or not. * Existential Value Check RegExp * Group 1 field field \w+ * Group 2 then operator .*? / <=|>=|==|!=|~ * Group 3 [else] value .*? * Group 4 --- then .*? * Group 5 --- [else] .*? */ $match = []; /* Would like to do * preg_match_all($pattern, $string, $matches); * to get all matches at once but that results in Segmentation * fault. Therefore iteratively: */ while (preg_match($pattern, $string, $match)) { $resolved = ''; $evalcond = !empty($entry[$match[1]]); $then = count($match) > 3 ? 4 : 2; $else = count($match) > 3 ? 5 : 3; if ( $evalcond && count($match) > 3 ) { if ( $match[2] === '==' ) { $evalcond = $entry[$match[1]] === $match[3]; } elseif ( $match[2] === '!=' ) { $evalcond = $entry[$match[1]] !== $match[3]; } elseif ( $match[2] === '<=' ) { $evalcond = is_numeric($entry[$match[1]]) && is_numeric($match[3]) && (int)$entry[$match[1]] <= (int)$match[3]; } elseif ( $match[2] === '>=' ) { $evalcond = is_numeric($entry[$match[1]]) && is_numeric($match[3]) && (int)$entry[$match[1]] >= (int)$match[3]; } elseif ( $match[2] === '~' ) { $evalcond = preg_match('/'.$match[3].'/', $entry[$match[1]]) > 0; } } if ( $evalcond ) { $resolved = $match[$then]; } elseif ( !empty($match[$else]) ) { $resolved = $match[$else]; } // Recurse to cope with nested conditions $resolved = $this->resolve_conditions($entry, $resolved); $string = str_replace($match[0], $resolved, $string); } return $string; } } ?>