blob: 85aaab916cea6e4a883a2576eaf38f648c54dfbb [file] [log] [blame]
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +01001<?php
2
3/*
4 * This file is part of Twig.
5 *
6 * (c) Fabien Potencier
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Twig;
13
14use Twig\Cache\CacheInterface;
15use Twig\Cache\FilesystemCache;
16use Twig\Cache\NullCache;
17use Twig\Error\Error;
18use Twig\Error\LoaderError;
19use Twig\Error\RuntimeError;
20use Twig\Error\SyntaxError;
21use Twig\Extension\CoreExtension;
22use Twig\Extension\EscaperExtension;
23use Twig\Extension\ExtensionInterface;
24use Twig\Extension\OptimizerExtension;
25use Twig\Loader\ArrayLoader;
26use Twig\Loader\ChainLoader;
27use Twig\Loader\LoaderInterface;
28use Twig\Node\ModuleNode;
29use Twig\Node\Node;
30use Twig\NodeVisitor\NodeVisitorInterface;
31use Twig\RuntimeLoader\RuntimeLoaderInterface;
32use Twig\TokenParser\TokenParserInterface;
33
34/**
35 * Stores the Twig configuration and renders templates.
36 *
37 * @author Fabien Potencier <fabien@symfony.com>
38 */
39class Environment
40{
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010041 public const VERSION = '3.4.3';
42 public const VERSION_ID = 30403;
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +010043 public const MAJOR_VERSION = 3;
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010044 public const MINOR_VERSION = 4;
45 public const RELEASE_VERSION = 3;
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +010046 public const EXTRA_VERSION = '';
47
48 private $charset;
49 private $loader;
50 private $debug;
51 private $autoReload;
52 private $cache;
53 private $lexer;
54 private $parser;
55 private $compiler;
56 private $globals = [];
57 private $resolvedGlobals;
58 private $loadedTemplates;
59 private $strictVariables;
60 private $templateClassPrefix = '__TwigTemplate_';
61 private $originalCache;
62 private $extensionSet;
63 private $runtimeLoaders = [];
64 private $runtimes = [];
65 private $optionsHash;
66
67 /**
68 * Constructor.
69 *
70 * Available options:
71 *
72 * * debug: When set to true, it automatically set "auto_reload" to true as
73 * well (default to false).
74 *
75 * * charset: The charset used by the templates (default to UTF-8).
76 *
77 * * cache: An absolute path where to store the compiled templates,
78 * a \Twig\Cache\CacheInterface implementation,
79 * or false to disable compilation cache (default).
80 *
81 * * auto_reload: Whether to reload the template if the original source changed.
82 * If you don't provide the auto_reload option, it will be
83 * determined automatically based on the debug value.
84 *
85 * * strict_variables: Whether to ignore invalid variables in templates
86 * (default to false).
87 *
88 * * autoescape: Whether to enable auto-escaping (default to html):
89 * * false: disable auto-escaping
90 * * html, js: set the autoescaping to one of the supported strategies
91 * * name: set the autoescaping strategy based on the template name extension
92 * * PHP callback: a PHP callback that returns an escaping strategy based on the template "name"
93 *
94 * * optimizations: A flag that indicates which optimizations to apply
95 * (default to -1 which means that all optimizations are enabled;
96 * set it to 0 to disable).
97 */
98 public function __construct(LoaderInterface $loader, $options = [])
99 {
100 $this->setLoader($loader);
101
102 $options = array_merge([
103 'debug' => false,
104 'charset' => 'UTF-8',
105 'strict_variables' => false,
106 'autoescape' => 'html',
107 'cache' => false,
108 'auto_reload' => null,
109 'optimizations' => -1,
110 ], $options);
111
112 $this->debug = (bool) $options['debug'];
113 $this->setCharset($options['charset'] ?? 'UTF-8');
114 $this->autoReload = null === $options['auto_reload'] ? $this->debug : (bool) $options['auto_reload'];
115 $this->strictVariables = (bool) $options['strict_variables'];
116 $this->setCache($options['cache']);
117 $this->extensionSet = new ExtensionSet();
118
119 $this->addExtension(new CoreExtension());
120 $this->addExtension(new EscaperExtension($options['autoescape']));
121 $this->addExtension(new OptimizerExtension($options['optimizations']));
122 }
123
124 /**
125 * Enables debugging mode.
126 */
127 public function enableDebug()
128 {
129 $this->debug = true;
130 $this->updateOptionsHash();
131 }
132
133 /**
134 * Disables debugging mode.
135 */
136 public function disableDebug()
137 {
138 $this->debug = false;
139 $this->updateOptionsHash();
140 }
141
142 /**
143 * Checks if debug mode is enabled.
144 *
145 * @return bool true if debug mode is enabled, false otherwise
146 */
147 public function isDebug()
148 {
149 return $this->debug;
150 }
151
152 /**
153 * Enables the auto_reload option.
154 */
155 public function enableAutoReload()
156 {
157 $this->autoReload = true;
158 }
159
160 /**
161 * Disables the auto_reload option.
162 */
163 public function disableAutoReload()
164 {
165 $this->autoReload = false;
166 }
167
168 /**
169 * Checks if the auto_reload option is enabled.
170 *
171 * @return bool true if auto_reload is enabled, false otherwise
172 */
173 public function isAutoReload()
174 {
175 return $this->autoReload;
176 }
177
178 /**
179 * Enables the strict_variables option.
180 */
181 public function enableStrictVariables()
182 {
183 $this->strictVariables = true;
184 $this->updateOptionsHash();
185 }
186
187 /**
188 * Disables the strict_variables option.
189 */
190 public function disableStrictVariables()
191 {
192 $this->strictVariables = false;
193 $this->updateOptionsHash();
194 }
195
196 /**
197 * Checks if the strict_variables option is enabled.
198 *
199 * @return bool true if strict_variables is enabled, false otherwise
200 */
201 public function isStrictVariables()
202 {
203 return $this->strictVariables;
204 }
205
206 /**
207 * Gets the current cache implementation.
208 *
209 * @param bool $original Whether to return the original cache option or the real cache instance
210 *
211 * @return CacheInterface|string|false A Twig\Cache\CacheInterface implementation,
212 * an absolute path to the compiled templates,
213 * or false to disable cache
214 */
215 public function getCache($original = true)
216 {
217 return $original ? $this->originalCache : $this->cache;
218 }
219
220 /**
221 * Sets the current cache implementation.
222 *
223 * @param CacheInterface|string|false $cache A Twig\Cache\CacheInterface implementation,
224 * an absolute path to the compiled templates,
225 * or false to disable cache
226 */
227 public function setCache($cache)
228 {
229 if (\is_string($cache)) {
230 $this->originalCache = $cache;
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100231 $this->cache = new FilesystemCache($cache, $this->autoReload ? FilesystemCache::FORCE_BYTECODE_INVALIDATION : 0);
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100232 } elseif (false === $cache) {
233 $this->originalCache = $cache;
234 $this->cache = new NullCache();
235 } elseif ($cache instanceof CacheInterface) {
236 $this->originalCache = $this->cache = $cache;
237 } else {
238 throw new \LogicException('Cache can only be a string, false, or a \Twig\Cache\CacheInterface implementation.');
239 }
240 }
241
242 /**
243 * Gets the template class associated with the given string.
244 *
245 * The generated template class is based on the following parameters:
246 *
247 * * The cache key for the given template;
248 * * The currently enabled extensions;
249 * * Whether the Twig C extension is available or not;
250 * * PHP version;
251 * * Twig version;
252 * * Options with what environment was created.
253 *
254 * @param string $name The name for which to calculate the template class name
255 * @param int|null $index The index if it is an embedded template
256 *
257 * @internal
258 */
259 public function getTemplateClass(string $name, int $index = null): string
260 {
261 $key = $this->getLoader()->getCacheKey($name).$this->optionsHash;
262
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100263 return $this->templateClassPrefix.hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $key).(null === $index ? '' : '___'.$index);
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100264 }
265
266 /**
267 * Renders a template.
268 *
269 * @param string|TemplateWrapper $name The template name
270 *
271 * @throws LoaderError When the template cannot be found
272 * @throws SyntaxError When an error occurred during compilation
273 * @throws RuntimeError When an error occurred during rendering
274 */
275 public function render($name, array $context = []): string
276 {
277 return $this->load($name)->render($context);
278 }
279
280 /**
281 * Displays a template.
282 *
283 * @param string|TemplateWrapper $name The template name
284 *
285 * @throws LoaderError When the template cannot be found
286 * @throws SyntaxError When an error occurred during compilation
287 * @throws RuntimeError When an error occurred during rendering
288 */
289 public function display($name, array $context = []): void
290 {
291 $this->load($name)->display($context);
292 }
293
294 /**
295 * Loads a template.
296 *
297 * @param string|TemplateWrapper $name The template name
298 *
299 * @throws LoaderError When the template cannot be found
300 * @throws RuntimeError When a previously generated cache is corrupted
301 * @throws SyntaxError When an error occurred during compilation
302 */
303 public function load($name): TemplateWrapper
304 {
305 if ($name instanceof TemplateWrapper) {
306 return $name;
307 }
308
309 return new TemplateWrapper($this, $this->loadTemplate($this->getTemplateClass($name), $name));
310 }
311
312 /**
313 * Loads a template internal representation.
314 *
315 * This method is for internal use only and should never be called
316 * directly.
317 *
318 * @param string $name The template name
319 * @param int $index The index if it is an embedded template
320 *
321 * @throws LoaderError When the template cannot be found
322 * @throws RuntimeError When a previously generated cache is corrupted
323 * @throws SyntaxError When an error occurred during compilation
324 *
325 * @internal
326 */
327 public function loadTemplate(string $cls, string $name, int $index = null): Template
328 {
329 $mainCls = $cls;
330 if (null !== $index) {
331 $cls .= '___'.$index;
332 }
333
334 if (isset($this->loadedTemplates[$cls])) {
335 return $this->loadedTemplates[$cls];
336 }
337
338 if (!class_exists($cls, false)) {
339 $key = $this->cache->generateKey($name, $mainCls);
340
341 if (!$this->isAutoReload() || $this->isTemplateFresh($name, $this->cache->getTimestamp($key))) {
342 $this->cache->load($key);
343 }
344
345 $source = null;
346 if (!class_exists($cls, false)) {
347 $source = $this->getLoader()->getSourceContext($name);
348 $content = $this->compileSource($source);
349 $this->cache->write($key, $content);
350 $this->cache->load($key);
351
352 if (!class_exists($mainCls, false)) {
353 /* Last line of defense if either $this->bcWriteCacheFile was used,
354 * $this->cache is implemented as a no-op or we have a race condition
355 * where the cache was cleared between the above calls to write to and load from
356 * the cache.
357 */
358 eval('?>'.$content);
359 }
360
361 if (!class_exists($cls, false)) {
362 throw new RuntimeError(sprintf('Failed to load Twig template "%s", index "%s": cache might be corrupted.', $name, $index), -1, $source);
363 }
364 }
365 }
366
367 $this->extensionSet->initRuntime();
368
369 return $this->loadedTemplates[$cls] = new $cls($this);
370 }
371
372 /**
373 * Creates a template from source.
374 *
375 * This method should not be used as a generic way to load templates.
376 *
377 * @param string $template The template source
378 * @param string $name An optional name of the template to be used in error messages
379 *
380 * @throws LoaderError When the template cannot be found
381 * @throws SyntaxError When an error occurred during compilation
382 */
383 public function createTemplate(string $template, string $name = null): TemplateWrapper
384 {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100385 $hash = hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $template, false);
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100386 if (null !== $name) {
387 $name = sprintf('%s (string template %s)', $name, $hash);
388 } else {
389 $name = sprintf('__string_template__%s', $hash);
390 }
391
392 $loader = new ChainLoader([
393 new ArrayLoader([$name => $template]),
394 $current = $this->getLoader(),
395 ]);
396
397 $this->setLoader($loader);
398 try {
399 return new TemplateWrapper($this, $this->loadTemplate($this->getTemplateClass($name), $name));
400 } finally {
401 $this->setLoader($current);
402 }
403 }
404
405 /**
406 * Returns true if the template is still fresh.
407 *
408 * Besides checking the loader for freshness information,
409 * this method also checks if the enabled extensions have
410 * not changed.
411 *
412 * @param int $time The last modification time of the cached template
413 */
414 public function isTemplateFresh(string $name, int $time): bool
415 {
416 return $this->extensionSet->getLastModified() <= $time && $this->getLoader()->isFresh($name, $time);
417 }
418
419 /**
420 * Tries to load a template consecutively from an array.
421 *
422 * Similar to load() but it also accepts instances of \Twig\Template and
423 * \Twig\TemplateWrapper, and an array of templates where each is tried to be loaded.
424 *
425 * @param string|TemplateWrapper|array $names A template or an array of templates to try consecutively
426 *
427 * @throws LoaderError When none of the templates can be found
428 * @throws SyntaxError When an error occurred during compilation
429 */
430 public function resolveTemplate($names): TemplateWrapper
431 {
432 if (!\is_array($names)) {
433 return $this->load($names);
434 }
435
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100436 $count = \count($names);
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100437 foreach ($names as $name) {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100438 if ($name instanceof Template) {
439 return $name;
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100440 }
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100441 if ($name instanceof TemplateWrapper) {
442 return $name;
443 }
444
445 if (1 !== $count && !$this->getLoader()->exists($name)) {
446 continue;
447 }
448
449 return $this->load($name);
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100450 }
451
452 throw new LoaderError(sprintf('Unable to find one of the following templates: "%s".', implode('", "', $names)));
453 }
454
455 public function setLexer(Lexer $lexer)
456 {
457 $this->lexer = $lexer;
458 }
459
460 /**
461 * @throws SyntaxError When the code is syntactically wrong
462 */
463 public function tokenize(Source $source): TokenStream
464 {
465 if (null === $this->lexer) {
466 $this->lexer = new Lexer($this);
467 }
468
469 return $this->lexer->tokenize($source);
470 }
471
472 public function setParser(Parser $parser)
473 {
474 $this->parser = $parser;
475 }
476
477 /**
478 * Converts a token stream to a node tree.
479 *
480 * @throws SyntaxError When the token stream is syntactically or semantically wrong
481 */
482 public function parse(TokenStream $stream): ModuleNode
483 {
484 if (null === $this->parser) {
485 $this->parser = new Parser($this);
486 }
487
488 return $this->parser->parse($stream);
489 }
490
491 public function setCompiler(Compiler $compiler)
492 {
493 $this->compiler = $compiler;
494 }
495
496 /**
497 * Compiles a node and returns the PHP code.
498 */
499 public function compile(Node $node): string
500 {
501 if (null === $this->compiler) {
502 $this->compiler = new Compiler($this);
503 }
504
505 return $this->compiler->compile($node)->getSource();
506 }
507
508 /**
509 * Compiles a template source code.
510 *
511 * @throws SyntaxError When there was an error during tokenizing, parsing or compiling
512 */
513 public function compileSource(Source $source): string
514 {
515 try {
516 return $this->compile($this->parse($this->tokenize($source)));
517 } catch (Error $e) {
518 $e->setSourceContext($source);
519 throw $e;
520 } catch (\Exception $e) {
521 throw new SyntaxError(sprintf('An exception has been thrown during the compilation of a template ("%s").', $e->getMessage()), -1, $source, $e);
522 }
523 }
524
525 public function setLoader(LoaderInterface $loader)
526 {
527 $this->loader = $loader;
528 }
529
530 public function getLoader(): LoaderInterface
531 {
532 return $this->loader;
533 }
534
535 public function setCharset(string $charset)
536 {
537 if ('UTF8' === $charset = null === $charset ? null : strtoupper($charset)) {
538 // iconv on Windows requires "UTF-8" instead of "UTF8"
539 $charset = 'UTF-8';
540 }
541
542 $this->charset = $charset;
543 }
544
545 public function getCharset(): string
546 {
547 return $this->charset;
548 }
549
550 public function hasExtension(string $class): bool
551 {
552 return $this->extensionSet->hasExtension($class);
553 }
554
555 public function addRuntimeLoader(RuntimeLoaderInterface $loader)
556 {
557 $this->runtimeLoaders[] = $loader;
558 }
559
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100560 /**
561 * @template TExtension of ExtensionInterface
562 *
563 * @param class-string<TExtension> $class
564 *
565 * @return TExtension
566 */
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100567 public function getExtension(string $class): ExtensionInterface
568 {
569 return $this->extensionSet->getExtension($class);
570 }
571
572 /**
573 * Returns the runtime implementation of a Twig element (filter/function/tag/test).
574 *
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100575 * @template TRuntime of object
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100576 *
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100577 * @param class-string<TRuntime> $class A runtime class name
578 *
579 * @return TRuntime The runtime implementation
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100580 *
581 * @throws RuntimeError When the template cannot be found
582 */
583 public function getRuntime(string $class)
584 {
585 if (isset($this->runtimes[$class])) {
586 return $this->runtimes[$class];
587 }
588
589 foreach ($this->runtimeLoaders as $loader) {
590 if (null !== $runtime = $loader->load($class)) {
591 return $this->runtimes[$class] = $runtime;
592 }
593 }
594
595 throw new RuntimeError(sprintf('Unable to load the "%s" runtime.', $class));
596 }
597
598 public function addExtension(ExtensionInterface $extension)
599 {
600 $this->extensionSet->addExtension($extension);
601 $this->updateOptionsHash();
602 }
603
604 /**
605 * @param ExtensionInterface[] $extensions An array of extensions
606 */
607 public function setExtensions(array $extensions)
608 {
609 $this->extensionSet->setExtensions($extensions);
610 $this->updateOptionsHash();
611 }
612
613 /**
614 * @return ExtensionInterface[] An array of extensions (keys are for internal usage only and should not be relied on)
615 */
616 public function getExtensions(): array
617 {
618 return $this->extensionSet->getExtensions();
619 }
620
621 public function addTokenParser(TokenParserInterface $parser)
622 {
623 $this->extensionSet->addTokenParser($parser);
624 }
625
626 /**
627 * @return TokenParserInterface[]
628 *
629 * @internal
630 */
631 public function getTokenParsers(): array
632 {
633 return $this->extensionSet->getTokenParsers();
634 }
635
636 /**
637 * @internal
638 */
639 public function getTokenParser(string $name): ?TokenParserInterface
640 {
641 return $this->extensionSet->getTokenParser($name);
642 }
643
644 public function registerUndefinedTokenParserCallback(callable $callable): void
645 {
646 $this->extensionSet->registerUndefinedTokenParserCallback($callable);
647 }
648
649 public function addNodeVisitor(NodeVisitorInterface $visitor)
650 {
651 $this->extensionSet->addNodeVisitor($visitor);
652 }
653
654 /**
655 * @return NodeVisitorInterface[]
656 *
657 * @internal
658 */
659 public function getNodeVisitors(): array
660 {
661 return $this->extensionSet->getNodeVisitors();
662 }
663
664 public function addFilter(TwigFilter $filter)
665 {
666 $this->extensionSet->addFilter($filter);
667 }
668
669 /**
670 * @internal
671 */
672 public function getFilter(string $name): ?TwigFilter
673 {
674 return $this->extensionSet->getFilter($name);
675 }
676
677 public function registerUndefinedFilterCallback(callable $callable): void
678 {
679 $this->extensionSet->registerUndefinedFilterCallback($callable);
680 }
681
682 /**
683 * Gets the registered Filters.
684 *
685 * Be warned that this method cannot return filters defined with registerUndefinedFilterCallback.
686 *
687 * @return TwigFilter[]
688 *
689 * @see registerUndefinedFilterCallback
690 *
691 * @internal
692 */
693 public function getFilters(): array
694 {
695 return $this->extensionSet->getFilters();
696 }
697
698 public function addTest(TwigTest $test)
699 {
700 $this->extensionSet->addTest($test);
701 }
702
703 /**
704 * @return TwigTest[]
705 *
706 * @internal
707 */
708 public function getTests(): array
709 {
710 return $this->extensionSet->getTests();
711 }
712
713 /**
714 * @internal
715 */
716 public function getTest(string $name): ?TwigTest
717 {
718 return $this->extensionSet->getTest($name);
719 }
720
721 public function addFunction(TwigFunction $function)
722 {
723 $this->extensionSet->addFunction($function);
724 }
725
726 /**
727 * @internal
728 */
729 public function getFunction(string $name): ?TwigFunction
730 {
731 return $this->extensionSet->getFunction($name);
732 }
733
734 public function registerUndefinedFunctionCallback(callable $callable): void
735 {
736 $this->extensionSet->registerUndefinedFunctionCallback($callable);
737 }
738
739 /**
740 * Gets registered functions.
741 *
742 * Be warned that this method cannot return functions defined with registerUndefinedFunctionCallback.
743 *
744 * @return TwigFunction[]
745 *
746 * @see registerUndefinedFunctionCallback
747 *
748 * @internal
749 */
750 public function getFunctions(): array
751 {
752 return $this->extensionSet->getFunctions();
753 }
754
755 /**
756 * Registers a Global.
757 *
758 * New globals can be added before compiling or rendering a template;
759 * but after, you can only update existing globals.
760 *
761 * @param mixed $value The global value
762 */
763 public function addGlobal(string $name, $value)
764 {
765 if ($this->extensionSet->isInitialized() && !\array_key_exists($name, $this->getGlobals())) {
766 throw new \LogicException(sprintf('Unable to add global "%s" as the runtime or the extensions have already been initialized.', $name));
767 }
768
769 if (null !== $this->resolvedGlobals) {
770 $this->resolvedGlobals[$name] = $value;
771 } else {
772 $this->globals[$name] = $value;
773 }
774 }
775
776 /**
777 * @internal
778 */
779 public function getGlobals(): array
780 {
781 if ($this->extensionSet->isInitialized()) {
782 if (null === $this->resolvedGlobals) {
783 $this->resolvedGlobals = array_merge($this->extensionSet->getGlobals(), $this->globals);
784 }
785
786 return $this->resolvedGlobals;
787 }
788
789 return array_merge($this->extensionSet->getGlobals(), $this->globals);
790 }
791
792 public function mergeGlobals(array $context): array
793 {
794 // we don't use array_merge as the context being generally
795 // bigger than globals, this code is faster.
796 foreach ($this->getGlobals() as $key => $value) {
797 if (!\array_key_exists($key, $context)) {
798 $context[$key] = $value;
799 }
800 }
801
802 return $context;
803 }
804
805 /**
806 * @internal
807 */
808 public function getUnaryOperators(): array
809 {
810 return $this->extensionSet->getUnaryOperators();
811 }
812
813 /**
814 * @internal
815 */
816 public function getBinaryOperators(): array
817 {
818 return $this->extensionSet->getBinaryOperators();
819 }
820
821 private function updateOptionsHash(): void
822 {
823 $this->optionsHash = implode(':', [
824 $this->extensionSet->getSignature(),
825 \PHP_MAJOR_VERSION,
826 \PHP_MINOR_VERSION,
827 self::VERSION,
828 (int) $this->debug,
829 (int) $this->strictVariables,
830 ]);
831 }
832}