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