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