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