Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
namespace core\output;
18
 
1441 ariadna 19
use Mustache_Tokenizer;
1 efrain 20
 
21
/**
22
 * Load template source strings.
23
 *
24
 * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
25
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1441 ariadna 26
 * @package core
1 efrain 27
 */
28
class mustache_template_source_loader {
29
    /** @var $gettemplatesource Callback function to load the template source from full name */
30
    private $gettemplatesource = null;
31
 
32
    /**
33
     * Constructor that takes a callback to allow the calling code to specify how to retrieve
34
     * the source for a template name.
35
     *
36
     * If no callback is provided then default to the load from disk implementation.
37
     *
38
     * @param callable|null $gettemplatesource Callback to load template source by template name
39
     */
1441 ariadna 40
    public function __construct(?callable $gettemplatesource = null) {
1 efrain 41
        if ($gettemplatesource) {
42
            // The calling code has specified a function for retrieving the template source
43
            // code by name and theme.
44
            $this->gettemplatesource = $gettemplatesource;
45
        } else {
46
            // By default we will pull the template from disk.
1441 ariadna 47
            $this->gettemplatesource = function ($component, $name, $themename) {
1 efrain 48
                $fulltemplatename = $component . '/' . $name;
49
                $filename = mustache_template_finder::get_template_filepath($fulltemplatename, $themename);
50
                return file_get_contents($filename);
51
            };
52
        }
53
    }
54
 
55
    /**
56
     * Remove comments from mustache template.
57
     *
58
     * @param string $templatestr
59
     * @return string
60
     */
61
    protected function strip_template_comments($templatestr): string {
62
        return preg_replace('/(?={{!)(.*)(}})/sU', '', $templatestr);
63
    }
64
 
65
    /**
66
     * Load the template source from the component and template name.
67
     *
68
     * @param string $component The moodle component (e.g. core_message)
69
     * @param string $name The template name (e.g. message_drawer)
70
     * @param string $themename The theme to load the template for (e.g. boost)
71
     * @param bool $includecomments If the comments should be stripped from the source before returning
72
     * @return string The template source
73
     */
74
    public function load(
75
        string $component,
76
        string $name,
77
        string $themename,
78
        bool $includecomments = false
79
    ): string {
80
        global $CFG;
81
        // Get the template source from the callback.
82
        $source = ($this->gettemplatesource)($component, $name, $themename);
83
 
84
        // Remove comments from template.
85
        if (!$includecomments) {
86
            $source = $this->strip_template_comments($source);
87
        }
88
        if (!empty($CFG->debugtemplateinfo)) {
89
            return "<!-- template(JS): $name -->" . $source . "<!-- /template(JS): $name -->";
90
        }
91
        return $source;
92
    }
93
 
94
    /**
95
     * Load a template and some of the dependencies that will be needed in order to render
96
     * the template.
97
     *
98
     * The current implementation will return all of the templates and all of the strings in
99
     * each of those templates (excluding string substitutions).
100
     *
101
     * The return format is an array indexed with the dependency type (e.g. templates / strings) then
102
     * the component (e.g. core_message), and then the id (e.g. message_drawer).
103
     *
104
     * For example:
105
     * * We have 3 templates in core named foo, bar, and baz.
106
     * * foo includes bar and bar includes baz.
107
     * * foo uses the string 'home' from core
108
     * * baz uses the string 'help' from core
109
     *
110
     * If we load the template foo this function would return:
111
     * [
112
     *      'templates' => [
113
     *          'core' => [
114
     *              'foo' => '... template source ...',
115
     *              'bar' => '... template source ...',
116
     *              'baz' => '... template source ...',
117
     *          ]
118
     *      ],
119
     *      'strings' => [
120
     *          'core' => [
121
     *              'home' => 'Home',
122
     *              'help' => 'Help'
123
     *          ]
124
     *      ]
125
     * ]
126
     *
127
     * @param string $templatecomponent The moodle component (e.g. core_message)
128
     * @param string $templatename The template name (e.g. message_drawer)
129
     * @param string $themename The theme to load the template for (e.g. boost)
130
     * @param bool $includecomments If the comments should be stripped from the source before returning
131
     * @param array $seentemplates List of templates already processed / to be skipped.
132
     * @param array $seenstrings List of strings already processed / to be skipped.
133
     * @param string|null $lang moodle translation language, null means use current.
134
     * @return array
135
     */
136
    public function load_with_dependencies(
137
        string $templatecomponent,
138
        string $templatename,
139
        string $themename,
140
        bool $includecomments = false,
141
        array $seentemplates = [],
142
        array $seenstrings = [],
1441 ariadna 143
        ?string $lang = null
1 efrain 144
    ): array {
145
        // Initialise the return values.
146
        $templates = [];
147
        $strings = [];
148
        $templatecomponent = trim($templatecomponent);
149
        $templatename = trim($templatename);
150
        // Get the requested template source.
151
        $templatesource = $this->load($templatecomponent, $templatename, $themename, $includecomments);
152
        // This is a helper function to save a value in one of the result arrays (either $templates or $strings).
1441 ariadna 153
        $save = function (array $results, array $seenlist, string $component, string $id, $value) use ($lang) {
1 efrain 154
            if (!isset($results[$component])) {
155
                // If the results list doesn't already contain this component then initialise it.
156
                $results[$component] = [];
157
            }
158
 
159
            // Save the value.
160
            $results[$component][$id] = $value;
161
            // Record that this item has been processed.
162
            array_push($seenlist, "$component/$id");
163
            // Return the updated results and seen list.
164
            return [$results, $seenlist];
165
        };
166
        // This is a helper function for processing a dependency. Does stuff like ignore duplicate processing,
167
        // common result formatting etc.
1441 ariadna 168
        $handler = function (array $dependency, array $ignorelist, callable $processcallback) use ($lang) {
1 efrain 169
            foreach ($dependency as $component => $ids) {
170
                foreach ($ids as $id) {
171
                    $dependencyid = "$component/$id";
172
                    if (array_search($dependencyid, $ignorelist) === false) {
173
                        $processcallback($component, $id);
174
                        // Add this to our ignore list now that we've processed it so that we don't
175
                        // process it again.
176
                        array_push($ignorelist, $dependencyid);
177
                    }
178
                }
179
            }
180
 
181
            return $ignorelist;
182
        };
183
 
184
        // Save this template as the first result in the $templates result array.
1441 ariadna 185
        [$templates, $seentemplates] = $save($templates, $seentemplates, $templatecomponent, $templatename, $templatesource);
1 efrain 186
 
187
        // Check the template for any dependencies that need to be loaded.
188
        $dependencies = $this->scan_template_source_for_dependencies($templatesource);
189
 
190
        // Load all of the lang strings that this template requires and add them to the
191
        // returned values.
192
        $seenstrings = $handler(
193
            $dependencies['strings'],
194
            $seenstrings,
195
            // Include $strings and $seenstrings by reference so that their values can be updated
196
            // outside of this anonymous function.
1441 ariadna 197
            function ($component, $id) use ($save, &$strings, &$seenstrings, $lang) {
1 efrain 198
                $string = get_string_manager()->get_string($id, $component, null, $lang);
199
                // Save the string in the $strings results array.
1441 ariadna 200
                [$strings, $seenstrings] = $save($strings, $seenstrings, $component, $id, $string);
1 efrain 201
            }
202
        );
203
 
204
        // Load any child templates that we've found in this template and add them to
205
        // the return list of dependencies.
206
        $seentemplates = $handler(
207
            $dependencies['templates'],
208
            $seentemplates,
209
            // Include $strings, $seenstrings, $templates, and $seentemplates by reference so that their values can be updated
210
            // outside of this anonymous function.
1441 ariadna 211
            function (
212
                $component,
213
                $id
214
            ) use (
1 efrain 215
                $themename,
216
                $includecomments,
217
                &$seentemplates,
218
                &$seenstrings,
219
                &$templates,
220
                &$strings,
221
                $save,
222
                $lang
223
            ) {
224
                // We haven't seen this template yet so load it and it's dependencies.
225
                $subdependencies = $this->load_with_dependencies(
226
                    $component,
227
                    $id,
228
                    $themename,
229
                    $includecomments,
230
                    $seentemplates,
231
                    $seenstrings,
232
                    $lang
233
                );
234
 
235
                foreach ($subdependencies['templates'] as $component => $ids) {
236
                    foreach ($ids as $id => $value) {
237
                        // Include the child themes in our results.
1441 ariadna 238
                        [$templates, $seentemplates] = $save($templates, $seentemplates, $component, $id, $value);
1 efrain 239
                    }
240
                };
241
 
242
                foreach ($subdependencies['strings'] as $component => $ids) {
243
                    foreach ($ids as $id => $value) {
244
                        // Include any strings that the child templates need in our results.
1441 ariadna 245
                        [$strings, $seenstrings] = $save($strings, $seenstrings, $component, $id, $value);
1 efrain 246
                    }
247
                }
248
            }
249
        );
250
 
251
        return [
252
            'templates' => $templates,
1441 ariadna 253
            'strings' => $strings,
1 efrain 254
        ];
255
    }
256
 
257
    /**
258
     * Scan over a template source string and return a list of dependencies it requires.
259
     * At the moment the list will only include other templates and strings.
260
     *
261
     * The return format is an array indexed with the dependency type (e.g. templates / strings) then
262
     * the component (e.g. core_message) with it's value being an array of the items required
263
     * in that component.
264
     *
265
     * For example:
266
     * If we have a template foo that includes 2 templates, bar and baz, and also 2 strings
267
     * 'home' and 'help' from the core component then the return value would look like:
268
     *
269
     * [
270
     *      'templates' => [
271
     *          'core' => ['foo', 'bar', 'baz']
272
     *      ],
273
     *      'strings' => [
274
     *          'core' => ['home', 'help']
275
     *      ]
276
     * ]
277
     *
278
     * @param string $source The template source
279
     * @return array
280
     */
281
    protected function scan_template_source_for_dependencies(string $source): array {
282
        $tokenizer = new Mustache_Tokenizer();
283
        $tokens = $tokenizer->scan($source);
284
        $templates = [];
285
        $strings = [];
1441 ariadna 286
        $addtodependencies = function ($dependencies, $component, $id) {
1 efrain 287
            $id = trim($id);
288
            $component = trim($component);
289
 
290
            if (!isset($dependencies[$component])) {
291
                // Initialise the component if we haven't seen it before.
292
                $dependencies[$component] = [];
293
            }
294
 
295
            // Add this id to the list of dependencies.
296
            array_push($dependencies[$component], $id);
297
 
298
            return $dependencies;
299
        };
300
 
301
        foreach ($tokens as $index => $token) {
302
            $type = $token['type'];
303
            $name = isset($token['name']) ? $token['name'] : null;
304
 
305
            if ($name) {
306
                switch ($type) {
307
                    case Mustache_Tokenizer::T_PARTIAL:
1441 ariadna 308
                        [$component, $id] = explode('/', $name, 2);
1 efrain 309
                        $templates = $addtodependencies($templates, $component, $id);
310
                        break;
311
                    case Mustache_Tokenizer::T_PARENT:
1441 ariadna 312
                        [$component, $id] = explode('/', $name, 2);
1 efrain 313
                        $templates = $addtodependencies($templates, $component, $id);
314
                        break;
315
                    case Mustache_Tokenizer::T_SECTION:
316
                        if ($name == 'str') {
1441 ariadna 317
                            [$id, $component] = $this->get_string_identifiers($tokens, $index);
1 efrain 318
 
319
                            if ($id) {
320
                                $strings = $addtodependencies($strings, $component, $id);
321
                            }
322
                        }
323
                        break;
324
                }
325
            }
326
        }
327
 
328
        return [
329
            'templates' => $templates,
1441 ariadna 330
            'strings' => $strings,
1 efrain 331
        ];
332
    }
333
 
334
    /**
335
     * Gets the identifier and component of the string.
336
     *
337
     * The string could be defined on one, or multiple lines.
338
     *
339
     * @param array $tokens The templates token.
340
     * @param int $start The index of the start of the string token.
341
     * @return array A list of the string identifier and component.
342
     */
343
    protected function get_string_identifiers(array $tokens, int $start): array {
344
        $current = $start + 1;
345
        $parts = [];
346
 
347
        // Get the contents of the string tag.
348
        while ($tokens[$current]['type'] !== Mustache_Tokenizer::T_END_SECTION) {
349
            if (!isset($tokens[$current]['value']) || empty(trim($tokens[$current]['value']))) {
350
                // An empty line, so we should ignore it.
351
                $current++;
352
                continue;
353
            }
354
 
355
            // We need to remove any spaces before and after the string.
356
            $nospaces = trim($tokens[$current]['value']);
357
 
358
            // We need to remove any trailing commas so that the explode will not add an
359
            // empty entry where two paramters are on multiple lines.
360
            $clean = rtrim($nospaces, ',');
361
 
362
            // We separate the parts of a string with commas.
363
            $subparts = explode(',', $clean);
364
 
365
            // Store the parts.
366
            $parts = array_merge($parts, $subparts);
367
 
368
            $current++;
369
        }
370
 
371
        // The first text should be the first part of a str tag.
372
        $id = isset($parts[0]) ? trim($parts[0]) : null;
373
 
374
        // Default to 'core' for the component, if not specified.
375
        $component = isset($parts[1]) ? trim($parts[1]) : 'core';
376
 
377
        return [$id, $component];
378
    }
379
}