Proyectos de Subversion Moodle

Rev

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