PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
← All changes | jetpack_vendor/automattic/jetpack-assets/src/class-assets.php +129 -41 12.3.2 → 16.3 View file →
@@ -7,8 +7,9 @@
7 7
8 8 namespace Automattic\Jetpack;
9 9
10 10 use Automattic\Jetpack\Assets\Semver;
11 +use Automattic\Jetpack\Assets\Shared_Stores_Assets;
11 12 use Automattic\Jetpack\Constants as Jetpack_Constants;
12 13 use InvalidArgumentException;
13 14
14 15 /**
@@ -31,13 +32,36 @@
31 32
32 33 /**
33 34 * The registered textdomain mappings.
34 35 *
35 - * @var array `array( mapped_domain => array( string target_domain, string target_type, string semver, string path_prefix ) )`.
36 + * @var array `array( mapped_domain => array( string target_domain, string target_type, string semver ) )`.
36 37 */
37 38 private static $domain_map = array();
38 39
39 40 /**
41 + * The registered package paths, by textdomain.
42 + *
43 + * Separate from `$domain_map` because the two answer different questions:
44 + * the map says which domain a package's strings are translated under, while
45 + * this says where the package's files live — the prefix WordPress hashes to
46 + * name a JS translation file. A package whose textdomain is already its
47 + * plugin's has nothing to alias but still needs the path.
48 + *
49 + * Note the entries are keyed by domain, not by script: `downloadI18n()`
50 + * prepends a domain's prefix to every bundle path looked up under it. That
51 + * is only ever one package's path, so a plugin must not both bundle a
52 + * package whose textdomain is the plugin's own and load its own
53 + * `wp-jp-i18n-loader` bundles under that same domain — the package's prefix
54 + * would be applied to the plugin's catalogs too, and they would all 404. No
55 + * plugin does both today. One that needs to should give the package a
56 + * distinct textdomain, the way `jetpack-backup-pkg` and
57 + * `jetpack-videopress-pkg` do.
58 + *
59 + * @var array `array( domain => array( string semver, string path_prefix ) )`.
60 + */
61 + private static $domain_paths = array();
62 +
63 + /**
40 64 * Constructor.
41 65 *
42 66 * Static-only class, so nothing here.
43 67 */
@@ -53,9 +77,8 @@
53 77 */
54 78 public static function instance() {
55 79 if ( ! isset( self::$instance ) ) {
56 80 self::$instance = new Assets();
57 - self::$instance->init_hooks();
58 81 }
59 82
60 83 return self::$instance;
61 84 }
@@ -60,25 +83,18 @@
60 83 return self::$instance;
61 84 }
62 85
63 86 /**
64 - * Initalize the hooks as needed.
65 - */
66 - private function init_hooks() {
67 - /*
68 - * Load some scripts asynchronously.
69 - */
70 - add_filter( 'script_loader_tag', array( $this, 'script_add_async' ), 10, 2 );
71 - }
72 -
73 - /**
74 87 * A public method for adding the async script.
75 88 *
89 + * @deprecated Since 2.1.0, the `strategy` feature should be used instead, with the "defer" setting.
90 + *
76 91 * @param string $script_handle Script handle.
77 92 */
78 93 public static function add_async_script( $script_handle ) {
79 - $assets_instance = self::instance();
80 - $assets_instance->defer_script_handles[] = $script_handle;
94 + _deprecated_function( __METHOD__, '2.1.0' );
95 +
96 + wp_script_add_data( $script_handle, 'strategy', 'defer' );
81 97 }
82 98
83 99 /**
84 100 * Add an async attribute to scripts that can be loaded deferred.
@@ -83,12 +99,15 @@
83 99 /**
84 100 * Add an async attribute to scripts that can be loaded deferred.
85 101 * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script
86 102 *
103 + * @deprecated Since 2.1.0, the `strategy` feature should be used instead.
104 + *
87 105 * @param string $tag The <script> tag for the enqueued script.
88 106 * @param string $handle The script's registered handle.
89 107 */
90 108 public function script_add_async( $tag, $handle ) {
109 + _deprecated_function( __METHOD__, '2.1.0' );
91 110 if ( empty( $this->defer_script_handles ) ) {
92 111 return $tag;
93 112 }
94 113
@@ -102,8 +121,10 @@
102 121
103 122 /**
104 123 * A helper function that lets you enqueue scripts in an async fashion.
105 124 *
125 + * @deprecated Since 2.1.0 - use the strategy feature instead.
126 + *
106 127 * @param string $handle Name of the script. Should be unique.
107 128 * @param string $min_path Minimized script path.
108 129 * @param string $non_min_path Full Script path.
109 130 * @param array $deps Array of script dependencies.
@@ -110,11 +131,11 @@
110 131 * @param bool $ver The script version.
111 132 * @param bool $in_footer Should the script be included in the footer.
112 133 */
113 134 public static function enqueue_async_script( $handle, $min_path, $non_min_path, $deps = array(), $ver = false, $in_footer = true ) {
114 - $assets_instance = self::instance();
115 - $assets_instance->add_async_script( $handle );
135 + _deprecated_function( __METHOD__, '2.1.0' );
116 136 wp_enqueue_script( $handle, self::get_file_url_for_environment( $min_path, $non_min_path ), $deps, $ver, $in_footer );
137 + wp_script_add_data( $handle, 'strategy', 'defer' );
117 138 }
118 139
119 140 // endregion .
120 141
@@ -259,13 +280,13 @@
259 280
260 281 $ret = '';
261 282 $ret .= isset( $parts['scheme'] ) ? $parts['scheme'] . '://' : '';
262 283 if ( isset( $parts['user'] ) || isset( $parts['pass'] ) ) {
263 - $ret .= isset( $parts['user'] ) ? $parts['user'] : '';
284 + $ret .= $parts['user'] ?? '';
264 285 $ret .= isset( $parts['pass'] ) ? ':' . $parts['pass'] : '';
265 286 $ret .= '@';
266 287 }
267 - $ret .= isset( $parts['host'] ) ? $parts['host'] : '';
288 + $ret .= $parts['host'] ?? '';
268 289 $ret .= isset( $parts['port'] ) ? ':' . $parts['port'] : '';
269 290
270 291 $pp = explode( '/', $parts['path'] );
271 292 if ( '' === $pp[0] ) {
@@ -306,14 +327,15 @@
306 327 *
307 328 * This wrapper handles all of that.
308 329 *
309 330 * @since 1.12.0
331 + * @since 2.1.0 Add a new `strategy` option to leverage WP >= 6.3 script strategy feature. The `async` option is deprecated.
310 332 * @param string $handle Name of the script. Should be unique across both scripts and styles.
311 333 * @param string $path Minimized script path.
312 334 * @param string $relative_to File that `$path` is relative to. Pass `__FILE__`.
313 335 * @param array $options Additional options:
314 336 * - `asset_path`: (string|null) `.asset.php` to load. Default is to base it on `$path`.
315 - * - `async`: (bool) Set true to register the script as async, like `Assets::enqueue_async_script()`
337 + * - `async`: (bool) Set true to register the script as deferred, like `Assets::enqueue_async_script()`. Deprecated in favor of `strategy`.
316 338 * - `css_dependencies`: (string[]) Additional style dependencies to queue.
317 339 * - `css_path`: (string|null) `.css` to load. Default is to base it on `$path`.
318 340 * - `dependencies`: (string[]) Additional script dependencies to queue.
319 341 * - `enqueue`: (bool) Set true to enqueue the script immediately.
@@ -320,10 +342,12 @@
320 342 * - `in_footer`: (bool) Set true to register script for the footer.
321 343 * - `media`: (string) Media for the css file. Default 'all'.
322 344 * - `minify`: (bool|null) Set true to pass `minify=true` in the query string, or `null` to suppress the normal `minify=false`.
323 345 * - `nonmin_path`: (string) Non-minified script path.
346 + * - `strategy`: (string) Specify a script strategy to use, eg. `defer` or `async`. Default is `""`.
324 347 * - `textdomain`: (string) Text domain for the script. Required if the script depends on wp-i18n.
325 348 * - `version`: (string) Override the version from the `asset_path` file.
349 + * @phan-param array{asset_path?:?string,async?:bool,css_dependencies?:string[],css_path?:?string,dependencies?:string[],enqueue?:bool,in_footer?:bool,media?:string,minify?:?bool,nonmin_path?:string,strategy?:string,textdomain?:string,version?:string} $options
326 350 * @throws \InvalidArgumentException If arguments are invalid.
327 351 */
328 352 public static function register_script( $handle, $path, $relative_to, array $options = array() ) {
329 353 if ( substr( $path, -3 ) !== '.js' ) {
@@ -329,8 +353,12 @@
329 353 if ( substr( $path, -3 ) !== '.js' ) {
330 354 throw new \InvalidArgumentException( '$path must end in ".js"' );
331 355 }
332 356
357 + if ( isset( $options['async'] ) ) {
358 + _deprecated_argument( __METHOD__, '2.1.0', 'The `async` option is deprecated in favor of `strategy`' );
359 + }
360 +
333 361 $dir = dirname( $relative_to );
334 362 $base = substr( $path, 0, -3 );
335 363 $options += array(
336 364 'asset_path' => "$base.asset.php",
@@ -341,12 +369,14 @@
341 369 'enqueue' => false,
342 370 'in_footer' => false,
343 371 'media' => 'all',
344 372 'minify' => false,
373 + 'strategy' => '',
345 374 'textdomain' => null,
346 375 );
376 + '@phan-var array{asset_path:?string,async:bool,css_dependencies:string[],css_path:?string,dependencies:string[],enqueue:bool,in_footer:bool,media:string,minify:?bool,nonmin_path?:string,strategy:string,textdomain:string,version?:string} $options'; // Phan gets confused by the array addition.
347 377
348 - if ( $options['css_path'] && substr( $options['css_path'], -4 ) !== '.css' ) {
378 + if ( is_string( $options['css_path'] ) && $options['css_path'] !== '' && substr( $options['css_path'], -4 ) !== '.css' ) {
349 379 throw new \InvalidArgumentException( '$options[\'css_path\'] must end in ".css"' );
350 380 }
351 381
352 382 if ( isset( $options['nonmin_path'] ) ) {
@@ -370,17 +400,28 @@
370 400 }
371 401 ),
372 402 $options['css_dependencies']
373 403 );
374 - $ver = isset( $options['version'] ) ? $options['version'] : $asset['version'];
404 + $ver = $options['version'] ?? $asset['version'];
375 405 } else {
376 - $ver = isset( $options['version'] ) ? $options['version'] : filemtime( "$dir/$path" );
406 + // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
407 + $ver = $options['version'] ?? @filemtime( "$dir/$path" );
377 408 }
378 409
379 - wp_register_script( $handle, $url, $options['dependencies'], $ver, $options['in_footer'] );
380 - if ( $options['async'] ) {
381 - self::instance()->add_async_script( $handle );
410 + if ( $options['async'] && '' === $options['strategy'] ) { // Handle the deprecated `async` option
411 + $options['strategy'] = 'defer';
382 412 }
413 + wp_register_script(
414 + $handle,
415 + $url,
416 + $options['dependencies'],
417 + $ver,
418 + array(
419 + 'in_footer' => $options['in_footer'],
420 + 'strategy' => $options['strategy'],
421 + )
422 + );
423 +
383 424 if ( $options['textdomain'] ) {
384 425 // phpcs:ignore Jetpack.Functions.I18n.DomainNotLiteral
385 426 wp_set_script_translations( $handle, $options['textdomain'] );
386 427 } elseif ( in_array( 'wp-i18n', $options['dependencies'], true ) ) {
@@ -391,9 +432,9 @@
391 432 ''
392 433 );
393 434 }
394 435
395 - if ( $options['css_path'] && file_exists( "$dir/{$options['css_path']}" ) ) {
436 + if ( is_string( $options['css_path'] ) && $options['css_path'] !== '' && file_exists( "$dir/{$options['css_path']}" ) ) {
396 437 $csspath = $options['css_path'];
397 438 if ( is_rtl() ) {
398 439 $rtlcsspath = substr( $csspath, 0, -4 ) . '.rtl.css';
399 440 if ( file_exists( "$dir/$rtlcsspath" ) ) {
@@ -429,8 +470,21 @@
429 470 }
430 471 }
431 472
432 473 /**
474 + * Re-hook the bootstraps an older copy's `actions.php` did not know about. See JETPACK-2649.
475 + *
476 + * Static callables only: `add_action()` dedupes those, but closures and object callables would
477 + * register twice. Callers must run before `wp_loaded`.
478 + *
479 + * @access private
480 + * @since 5.0.5
481 + */
482 + public static function ensure_package_bootstrap() {
483 + Shared_Stores_Assets::configure();
484 + }
485 +
486 + /**
433 487 * 'wp_default_scripts' action handler.
434 488 *
435 489 * This registers the `wp-jp-i18n-loader` script for use by Webpack bundles built with
436 490 * `@automattic/i18n-loader-webpack-plugin`.
@@ -449,8 +503,9 @@
449 503 $lang_dir = Jetpack_Constants::get_constant( 'WP_LANG_DIR' );
450 504 $content_dir = Jetpack_Constants::get_constant( 'WP_CONTENT_DIR' );
451 505 $abspath = Jetpack_Constants::get_constant( 'ABSPATH' );
452 506
507 + // Note: str_starts_with() is not used here, as wp-includes/compat.php may not be loaded at this point.
453 508 if ( strpos( $lang_dir, $content_dir ) === 0 ) {
454 509 $data['baseUrl'] = content_url( substr( trailingslashit( $lang_dir ), strlen( trailingslashit( $content_dir ) ) ) );
455 510 } elseif ( strpos( $lang_dir, $abspath ) === 0 ) {
456 511 $data['baseUrl'] = site_url( substr( trailingslashit( $lang_dir ), strlen( untrailingslashit( $abspath ) ) ) );
@@ -455,10 +510,12 @@
455 510 } elseif ( strpos( $lang_dir, $abspath ) === 0 ) {
456 511 $data['baseUrl'] = site_url( substr( trailingslashit( $lang_dir ), strlen( untrailingslashit( $abspath ) ) ) );
457 512 }
458 513
459 - foreach ( self::$domain_map as $from => list( $to, $type, , $path ) ) {
514 + foreach ( self::$domain_map as $from => list( $to, $type ) ) {
460 515 $data['domainMap'][ $from ] = ( 'core' === $type ? '' : "{$type}/" ) . $to;
516 + }
517 + foreach ( self::$domain_paths as $from => list( , $path ) ) {
461 518 if ( '' !== $path ) {
462 519 $data['domainPaths'][ $from ] = trailingslashit( $path );
463 520 }
464 521 }
@@ -491,9 +548,16 @@
491 548 );
492 549 }
493 550 $url = self::normalize_path( plugins_url( $path, __FILE__ ) );
494 551 $url = add_query_arg( 'minify', 'true', $url );
495 - $wp_scripts->add( 'wp-jp-i18n-loader', $url, $asset['dependencies'], $asset['version'] );
552 +
553 + $handle = 'wp-jp-i18n-loader';
554 +
555 + $wp_scripts->add( $handle, $url, $asset['dependencies'], $asset['version'] );
556 +
557 + // Ensure the script is loaded in the footer and deferred.
558 + $wp_scripts->add_data( $handle, 'group', 1 );
559 +
496 560 if ( ! is_array( $data ) ||
497 561 ! isset( $data['baseUrl'] ) || ! ( is_string( $data['baseUrl'] ) || false === $data['baseUrl'] ) ||
498 562 ! isset( $data['locale'] ) || ! is_string( $data['locale'] ) ||
499 563 ! isset( $data['domainMap'] ) || ! is_array( $data['domainMap'] ) ||
@@ -498,19 +562,19 @@
498 562 ! isset( $data['locale'] ) || ! is_string( $data['locale'] ) ||
499 563 ! isset( $data['domainMap'] ) || ! is_array( $data['domainMap'] ) ||
500 564 ! isset( $data['domainPaths'] ) || ! is_array( $data['domainPaths'] )
501 565 ) {
502 - $wp_scripts->add_inline_script( 'wp-jp-i18n-loader', 'console.warn( "I18n state deleted by jetpack_i18n_state hook" );' );
566 + $wp_scripts->add_inline_script( $handle, 'console.warn( "I18n state deleted by jetpack_i18n_state hook" );' );
503 567 } elseif ( ! $data['baseUrl'] ) {
504 - $wp_scripts->add_inline_script( 'wp-jp-i18n-loader', 'console.warn( "Failed to determine languages base URL. Is WP_LANG_DIR in the WordPress root?" );' );
568 + $wp_scripts->add_inline_script( $handle, 'console.warn( "Failed to determine languages base URL. Is WP_LANG_DIR in the WordPress root?" );' );
505 569 } else {
506 570 $data['domainMap'] = (object) $data['domainMap']; // Ensure it becomes a json object.
507 571 $data['domainPaths'] = (object) $data['domainPaths']; // Ensure it becomes a json object.
508 - $wp_scripts->add_inline_script( 'wp-jp-i18n-loader', 'wp.jpI18nLoader.state = ' . wp_json_encode( $data, JSON_UNESCAPED_SLASHES ) . ';' );
572 + $wp_scripts->add_inline_script( $handle, 'wp.jpI18nLoader.state = ' . wp_json_encode( $data, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) . ';' );
509 573 }
510 574
511 575 // Deprecated state module: Depend on wp-i18n to ensure global `wp` exists and because anything needing this will need that too.
512 - $wp_scripts->add( 'wp-jp-i18n-state', null, array( 'wp-deprecated', 'wp-jp-i18n-loader' ) );
576 + $wp_scripts->add( 'wp-jp-i18n-state', false, array( 'wp-deprecated', $handle ) );
513 577 $wp_scripts->add_inline_script( 'wp-jp-i18n-state', 'wp.deprecated( "wp-jp-i18n-state", { alternative: "wp-jp-i18n-loader" } );' );
514 578 $wp_scripts->add_inline_script( 'wp-jp-i18n-state', 'wp.jpI18nState = wp.jpI18nLoader.state;' );
515 579 }
516 580
@@ -559,13 +623,30 @@
559 623 ''
560 624 );
561 625 }
562 626
627 + // Where the package lives is needed for JS translation files whether or
628 + // not its domain is aliased, so it is recorded before the self-alias
629 + // check below.
630 + if (
631 + empty( self::$domain_paths[ $from ] ) ||
632 + Semver::compare( $ver, self::$domain_paths[ $from ][0] ) > 0
633 + ) {
634 + self::$domain_paths[ $from ] = array( $ver, $path );
635 + }
636 +
637 + // A self-alias would make filter_gettext() re-translate into the same
638 + // domain, recursing infinitely on any untranslated string (a package
639 + // textdomain can collide with its containing plugin's slug).
640 + if ( $from === $to ) {
641 + return;
642 + }
643 +
563 644 if ( empty( self::$domain_map[ $from ] ) ) {
564 645 self::init_domain_map_hooks( $from, array() === self::$domain_map );
565 - self::$domain_map[ $from ] = array( $to, $totype, $ver, $path );
646 + self::$domain_map[ $from ] = array( $to, $totype, $ver );
566 647 } elseif ( Semver::compare( $ver, self::$domain_map[ $from ][2] ) > 0 ) {
567 - self::$domain_map[ $from ] = array( $to, $totype, $ver, $path );
648 + self::$domain_map[ $from ] = array( $to, $totype, $ver );
568 649 }
569 650 }
570 651
571 652 /**
@@ -575,8 +656,11 @@
575 656 * with the following properties:
576 657 * - 'domain': String, `$to`
577 658 * - 'type': String, `$totype`
578 659 * - 'packages': Array, mapping `$from` to `array( 'path' => $path, 'ver' => $ver )` (or to the string `$ver` for back compat).
660 + * - 'paths': Array, same shape, for packages whose textdomain is already
661 + * `$to`. Those must not be aliased — that would recurse — but their
662 + * paths are still needed to locate their JavaScript translations.
579 663 *
580 664 * @since 1.15.0
581 665 * @param string $file Mapping file.
582 666 */
@@ -590,8 +674,13 @@
590 674 );
591 675 }
592 676 self::alias_textdomain( $from, $data['domain'], $data['type'], $fromdata['ver'], $fromdata['path'] );
593 677 }
678 + // Aliasing a domain to itself is a no-op that `alias_textdomain()`
679 + // declines, leaving just the path registration these entries are for.
680 + foreach ( $data['paths'] ?? array() as $from => $fromdata ) {
681 + self::alias_textdomain( $from, $from, $data['type'], $fromdata['ver'], $fromdata['path'] );
682 + }
594 683 }
595 684
596 685 /**
597 686 * Register the hooks for textdomain aliasing.
@@ -634,9 +723,9 @@
634 723 * @return string Translated text.
635 724 */
636 725 public static function filter_gettext( $translation, $text, $domain ) {
637 726 if ( $translation === $text ) {
638 - // phpcs:ignore WordPress.WP.I18n
727 + // phpcs:ignore WordPress.WP.I18n -- This is a filter hook to map the text domains from our Composer packages to the domain for a containing plugin. See https://wp.me/p2gHKz-oRh#problem-6-text-domains-in-composer-packages
639 728 $newtext = __( $text, self::$domain_map[ $domain ][0] );
640 729 if ( $newtext !== $text ) {
641 730 return $newtext;
642 731 }
@@ -650,15 +739,15 @@
650 739 * @since 1.15.0
651 740 * @param string $translation Translated text.
652 741 * @param string $single The text to be used if the number is singular.
653 742 * @param string $plural The text to be used if the number is plural.
654 - * @param string $number The number to compare against to use either the singular or plural form.
743 + * @param int $number The number to compare against to use either the singular or plural form.
655 744 * @param string $domain Text domain.
656 745 * @return string Translated text.
657 746 */
658 747 public static function filter_ngettext( $translation, $single, $plural, $number, $domain ) {
659 748 if ( $translation === $single || $translation === $plural ) {
660 - // phpcs:ignore WordPress.WP.I18n
749 + // phpcs:ignore WordPress.WP.I18n -- This is a filter hook to map the text domains from our Composer packages to the domain for a containing plugin. See https://wp.me/p2gHKz-oRh#problem-6-text-domains-in-composer-packages
661 750 $translation = _n( $single, $plural, $number, self::$domain_map[ $domain ][0] );
662 751 }
663 752 return $translation;
664 753 }
@@ -674,9 +763,9 @@
674 763 * @return string Translated text.
675 764 */
676 765 public static function filter_gettext_with_context( $translation, $text, $context, $domain ) {
677 766 if ( $translation === $text ) {
678 - // phpcs:ignore WordPress.WP.I18n
767 + // phpcs:ignore WordPress.WP.I18n -- This is a filter hook to map the text domains from our Composer packages to the domain for a containing plugin. See https://wp.me/p2gHKz-oRh#problem-6-text-domains-in-composer-packages
679 768 $translation = _x( $text, $context, self::$domain_map[ $domain ][0] );
680 769 }
681 770 return $translation;
682 771 }
@@ -687,9 +776,9 @@
687 776 * @since 1.15.0
688 777 * @param string $translation Translated text.
689 778 * @param string $single The text to be used if the number is singular.
690 779 * @param string $plural The text to be used if the number is plural.
691 - * @param string $number The number to compare against to use either the singular or plural form.
780 + * @param int $number The number to compare against to use either the singular or plural form.
692 781 * @param string $context Context information for the translators.
693 782 * @param string $domain Text domain.
694 783 * @return string Translated text.
695 784 */
@@ -694,9 +783,9 @@
694 783 * @return string Translated text.
695 784 */
696 785 public static function filter_ngettext_with_context( $translation, $single, $plural, $number, $context, $domain ) {
697 786 if ( $translation === $single || $translation === $plural ) {
698 - // phpcs:ignore WordPress.WP.I18n
787 + // phpcs:ignore WordPress.WP.I18n -- This is a filter hook to map the text domains from our Composer packages to the domain for a containing plugin. See https://wp.me/p2gHKz-oRh#problem-6-text-domains-in-composer-packages
699 788 $translation = _nx( $single, $plural, $number, $context, self::$domain_map[ $domain ][0] );
700 789 }
701 790 return $translation;
702 791 }
@@ -730,9 +819,8 @@
730 819 return $file;
731 820 }
732 821
733 822 // endregion .
734 -
735 823 }
736 824
737 825 // Enable section folding in vim:
738 826 // vim: foldmarker=//\ region,//\ endregion foldmethod=marker