Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body