Plugin Directory

Changeset 3412704


Ignore:
Timestamp:
12/06/2025 12:41:22 AM (2 months ago)
Author:
codealfa
Message:

Changes for version 5.1.1

Location:
jch-optimize/trunk
Files:
49 edited

Legend:

Unmodified
Added
Removed
  • jch-optimize/trunk/jch-optimize.php

    r3402713 r3412704  
    55 * Plugin URI: http://www.jch-optimize.net/
    66 * Description: Boost your WordPress site's performance with JCH Optimize as measured on PageSpeed
    7  * Version: 5.1.0
     7 * Version: 5.1.1
    88 * Author: Samuel Marshall
    99 * License: GNU/GPLv3
     
    1111 * Domain Path: /languages
    1212 * Requires PHP: 8.0
    13  * Requires at least: 5.0
     13 * Requires at least: 6.5.0
    1414 */
    1515
     
    3333        $message = sprintf(
    3434            __(
    35                 'JCH Optimize requires at least PHP 8.0. You current version is %s. Please update your PHP version or deactivate the plugin.',
     35                'JCH Optimize requires at least PHP 8.0.'
     36                . ' Your current version is %s. Please update your PHP version or deactivate the plugin.',
    3637                'jch-optimize'
    3738            ),
     
    8687    {
    8788        $message = __(
    88             'An error occurred while trying to initialize the JCH Optimize plugin. Please deactivate the plugin and report all errors to the developer.',
     89            'An error occurred while trying to initialize the JCH Optimize plugin.'
     90            . ' Please deactivate the plugin and report all errors to the developer.',
    8991            'jch-optimize'
    9092        );
  • jch-optimize/trunk/lib/src/Admin/Ajax/Ajax.php

    r3310120 r3412704  
    2323use JchOptimize\Core\Admin\AdminHelper;
    2424use JchOptimize\Core\Admin\Json;
     25use JchOptimize\Core\Optimize;
    2526use JchOptimize\Core\Platform\PathsInterface;
    2627use JchOptimize\Core\Platform\UtilityInterface;
     
    5657    protected function __construct()
    5758    {
    58         ini_set('pcre.backtrack_limit', '1000000');
    59         ini_set('pcre.recursion_limit', '1000000');
     59        Optimize::setPcreLimits();
    6060
    6161        if (!JCH_DEVELOP) {
    6262            error_reporting(0);
    6363            @ini_set('display_errors', 'Off');
    64         }
    65 
    66         if (version_compare(PHP_VERSION, '7.0.0', '>=')) {
    67             ini_set('pcre.jit', '0');
    6864        }
    6965
  • jch-optimize/trunk/lib/src/Cdn/Cdn.php

    r3310120 r3412704  
    122122                $staticFiles1Array = array_merge($staticFiles1Array, $customExtArray);
    123123
    124                 $this->domains->attach(new CdnDomain($domain1, $staticFiles1Array, $this->getScheme()));
     124                $this->domains->offsetSet(new CdnDomain($domain1, $staticFiles1Array, $this->getScheme()));
    125125            }
    126126
  • jch-optimize/trunk/lib/src/Css/Callbacks/AbstractCallback.php

    r3402713 r3412704  
    3737
    3838    protected array $conditionalAtRules = [
    39         'media' => true,
    40         'supports' => true,
    41         'layer' => true,
    42         'scope' => true,
    43         'container' => true,
    44         'document' => true
     39        'media',
     40        'supports',
     41        'layer',
     42        'scope',
     43        'container',
     44        'document',
    4545    ];
    4646
     
    6060
    6161    private ?CriticalCssDomainProfiler $profiler = null;
     62
     63    private string $conditionalAtRulesRegex;
    6264
    6365    public function __construct(Container $container, protected Registry $params)
     
    6668        $this->cacheObject = new CacheObject();
    6769        $this->supportedComponents = $this->supportedCssComponents();
     70        $this->conditionalAtRulesRegex = '^@(?:' . implode('|', $this->conditionalAtRules) . ')';
    6871    }
    6972
     
    100103        }
    101104
    102         try {
     105        if (preg_match("#{$this->conditionalAtRulesRegex}#", $matches[0])) {
    103106            $n = 'nesting_at_rule_load';
    104107            $this->profiler?->start($n);
    105             $nestingAtRule = NestingAtRule::load($matches[0]);
     108            $nestingAtRule = NestingAtRule::loadFromMatch($matches);
    106109            $this->profiler?->stop($n);
    107110
    108             if (isset($this->conditionalAtRules[$nestingAtRule->getIdentifier()])) {
    109                 $this->incrementRecursion();
    110                 $processedContent = $this->getParser()->processMatchesWithCallback(
    111                     $nestingAtRule->getCssRuleList(),
    112                     $this
    113                 );
    114                 $this->decrementRecursion($nestingAtRule);
    115 
    116                 return $nestingAtRule->setCssRuleList($processedContent)->render();
    117             }
    118         } catch (InvalidArgumentException) {
     111            $this->incrementRecursion();
     112            $processedContent = $this->getParser()->processMatchesWithCallback(
     113                $nestingAtRule->getCssRuleList(),
     114                $this
     115            );
     116            $this->decrementRecursion($nestingAtRule);
     117
     118            return $nestingAtRule->setCssRuleList($processedContent)->render();
    119119        }
    120120
     
    123123                $d = 'component_load';
    124124                $this->profiler?->start($d);
    125                 $cssComponent = $component::load($matches[0]);
     125                if (method_exists($component, 'loadFromMatch')) {
     126                    $cssComponent = $component::loadFromMatch($matches);
     127                } else {
     128                    $cssComponent = $component::load($matches[0]);
     129                }
    126130                $this->profiler?->stop($d);
    127131            } catch (InvalidArgumentException) {
  • jch-optimize/trunk/lib/src/Css/Callbacks/CorrectUrls.php

    r3402713 r3412704  
    103103    {
    104104        if (
    105             $originalUri->getScheme() !== 'data'
    106             && $originalUri->getPath() != ''
    107             && $originalUri->getPath() != '/'
    108         ) {
    109             $imageUri = $this->resolveImageToFileUrl($originalUri);
    110             $paths = $this->getContainer()->get(PathsInterface::class);
    111 
    112             if (UriComparator::existsLocally($imageUri, $this->cdn, $paths)) {
    113                 $this->cacheImageForAdminUsers($imageUri);
    114                 $imageUri = $this->cdn->loadCdnResource($imageUri);
    115             } elseif ($this->params->get('pro_preconnect_domains_enable', '0')) {
    116                 $this->prefetchExternalDomains($imageUri);
    117             }
    118 
    119             if ($this->context == 'css-rule') {
    120                 $imageUri = $this->applyFeatureHelpers($imageUri);
    121             }
    122 
    123             $this->addHttpPreloadsToCacheObject($imageUri);
    124 
    125             return $imageUri;
    126         }
    127 
    128         return $originalUri;
     105            $originalUri->getScheme() === 'data'
     106            || trim($originalUri->getPath(), " \n\r\t\v\x00/") === ''
     107        ) {
     108            return $originalUri;
     109        }
     110
     111        $imageUri = $this->resolveImageToFileUrl($originalUri);
     112        $paths = $this->getContainer()->get(PathsInterface::class);
     113
     114        if (UriComparator::existsLocally($imageUri, $this->cdn, $paths)) {
     115            $this->cacheImageForAdminUsers($imageUri);
     116            $imageUri = $this->cdn->loadCdnResource($imageUri);
     117        } elseif ($this->params->get('pro_preconnect_domains_enable', '0')) {
     118            $this->prefetchExternalDomains($imageUri);
     119        }
     120
     121        if ($this->context == 'css-rule') {
     122            $imageUri = $this->applyFeatureHelpers($imageUri);
     123        }
     124
     125        $this->addHttpPreloadsToCacheObject($imageUri);
     126
     127        return $imageUri;
    129128    }
    130129
     
    134133        $cssFileUri = $this->getCssInfo()->hasUri() ? $this->getCssInfo()->getUri() : new Uri();
    135134        $cssFileUri = UriResolver::resolve(SystemUri::currentUri(), $cssFileUri);
     135
    136136        return UriResolver::resolve($cssFileUri, $originalUri);
    137137    }
     
    165165            $responsiveImages = $this->getContainer()->get(ResponsiveImages::class)
    166166                /** @see ResponsiveImages::getResponsiveImages() */
    167                 ->getResponsiveImages($imageUri);
     167                                     ->getResponsiveImages($imageUri);
    168168        }
    169169
     
    184184            $this->getContainer()->get(LCPImages::class)
    185185                /** @see LCPImages::prepareBackgroundLcpImages() */
    186                 ->prepareBackgroundLcpImages($imageUri, $this->cacheObject);
     186                 ->prepareBackgroundLcpImages($imageUri, $this->cacheObject);
    187187        }
    188188
     
    200200            $this->getContainer()->get(LazyLoadExtended::class)
    201201                /** @see LazyLoadExtended::handleCssBgImages() */
    202                 ->handleCssBgImages(
    203                     $this,
    204                     $cssRule,
    205                     $lazyLoaded
    206                 );
     202                 ->handleCssBgImages(
     203                     $this,
     204                     $cssRule,
     205                     $lazyLoaded
     206                 );
    207207        }
    208208    }
     
    230230                [
    231231                    'src' => $imageUri,
    232                     'as' => $fileType
     232                    'as'  => $fileType
    233233                ]
    234234            ];
     
    236236                $cacheItems = $this->getContainer()->get(ResponsiveImages::class)
    237237                    /** @see ResponsiveImages::mergeResponsiveImageCacheItems() */
    238                     ->mergeResponsiveImageCacheItems($cacheItems);
     238                                   ->mergeResponsiveImageCacheItems($cacheItems);
    239239            }
    240240            foreach ($cacheItems as $cacheItem) {
     
    261261            $this->getContainer()->get(ResponsiveImages::class)
    262262                /** @see ResponsiveImages::makeCssRuleResponsive() */
    263                 ->makeCssRuleResponsive($cssRule);
     263                 ->makeCssRuleResponsive($cssRule);
    264264        }
    265265    }
  • jch-optimize/trunk/lib/src/Css/Callbacks/ExtractCriticalCss.php

    r3402713 r3412704  
    169169        $r = 'xpath_render';
    170170        $profiler?->start($r);
    171         $xPath = $cssSelectorXpath->render();
     171        $xPath = $cssSelectorXpath->renderFirstPerBranch();
    172172        $profiler?->stop($r);
    173173
     
    253253        $profiler = $this->dependencies->getProfiler();
    254254        $profiler?->start($normal = 'selector_normalize');
    255         $normalized = preg_replace('#\s+#', '', strtolower($selectorList));
     255        $normalized = preg_replace('#\s*([,>+~])\s*#', '\1', strtolower($selectorList));
    256256        $profiler?->stop($normal);
    257257
  • jch-optimize/trunk/lib/src/Css/Components/CssRule.php

    r3402713 r3412704  
    5252        }
    5353
    54         $selectorList = $matches['selectorlist'];
    55         $declarationList = $matches['declarationlist'];
     54        return self::loadFromMatch($matches);
     55    }
     56
     57    public static function loadFromMatch(array $matches): static
     58    {
     59        // Fallback to old behaviour if groups are missing.
     60        if (!isset($matches['selectorList'], $matches['declarationList'])) {
     61            return static::load($matches[0]);
     62        }
     63
     64        $selectorList = $matches['selectorList'];
     65        $declarationList = $matches['declarationList'];
    5666
    5767        return new static($selectorList, $declarationList);
     
    6777    }
    6878
    69     private static function cssRuleWithCaptureValueToken(): string
     79    public static function cssRuleWithCaptureValueToken(): string
    7080    {
    7181        $selectors = self::cssSelectorListToken();
    72 
    73         return "(?<selectorlist>{$selectors}){(?<declarationlist>.*)}";
     82        $declarations = self::cssDeclarationListToken();
     83
     84        return "(?<selectorList>{$selectors}){(?<declarationList>{$declarations})}";
    7485    }
    7586
  • jch-optimize/trunk/lib/src/Css/Components/CssSelector.php

    r3402713 r3412704  
    190190            $classes = new ClassCollection();
    191191            foreach ($this->classes as $class) {
    192                 $classes->attach(clone $class);
     192                $classes->offsetSet(clone $class);
    193193            }
    194194            $this->classes = $classes;
     
    198198            $attributes = new AttributeCollection();
    199199            foreach ($this->attributes as $attribute) {
    200                 $attributes->attach(clone $attribute);
     200                $attributes->offsetSet(clone $attribute);
    201201            }
    202202            $this->attributes = $attributes;
     
    206206            $pseudoClasses = new PseudoClassCollection();
    207207            foreach ($this->pseudoClasses as $pseudoSelector) {
    208                 $pseudoClasses->attach(clone $pseudoSelector);
     208                $pseudoClasses->offsetSet(clone $pseudoSelector);
    209209            }
    210210            $this->pseudoClasses = $pseudoClasses;
  • jch-optimize/trunk/lib/src/Css/Components/CssSelectorList.php

    r3402713 r3412704  
    6767
    6868        foreach ($this->selectors as $selector) {
    69             $selectors->attach(clone $selector);
     69            $selectors->offsetSet(clone $selector);
    7070        }
    7171
  • jch-optimize/trunk/lib/src/Css/Components/CssUrl.php

    r3310120 r3412704  
    8080
    8181        return "url\(\s*+(?<delimiter>['\"]?)(?<url>"
    82          . "(?<=\")(?>[^\"\\\\]++|{$esc})++|(?<=')(?>[^'\\\\]++|{$esc})++|(?>[^)\\\\]++|{$esc})++"
     82         . "(?<=\")(?>[^\"\\\\]++|{$esc})++|(?<=')(?>[^'\\\\]++|{$esc})++|(?>[^)\\\\]++|{$esc})*?"
    8383        . ")['\"]?\s*+\)";
    8484    }
  • jch-optimize/trunk/lib/src/Css/Components/NestingAtRule.php

    r3310120 r3412704  
    44 * JCH Optimize - Performs several front-end optimizations for fast downloads
    55 *
    6  *  @package   jchoptimize/core
    7  *  @author    Samuel Marshall <[email protected]>
    8  *  @copyright Copyright (c) 2024 Samuel Marshall / JCH Optimize
    9  *  @license   GNU/GPLv3, or later. See LICENSE file
     6 * @package   jchoptimize/core
     7 * @author    Samuel Marshall <[email protected]>
     8 * @copyright Copyright (c) 2024 Samuel Marshall / JCH Optimize
     9 * @license   GNU/GPLv3, or later. See LICENSE file
    1010 *
    1111 *  If LICENSE file missing, see <http://www.gnu.org/licenses/>.
     
    5050        }
    5151
     52        return self::loadFromMatch($matches);
     53    }
     54
     55    public static function loadFromMatch(array $matches): static
     56    {
     57        // If for some reason we didn't get the groups, fall back.
     58        if (!isset($matches['identifier'], $matches['rule'], $matches['cssRuleList'])) {
     59            return static::load($matches[0]);
     60        }
     61
    5262        $identifier = $matches['identifier'];
    53         $vendor = $matches['vendor'];
     63        $vendor = $matches['vendor'] ?? '';
    5464        $rule = $matches['rule'];
    5565        $cssRuleList = $matches['cssRuleList'];
     
    6373    }
    6474
    65 
    66     private static function cssNestingAtRuleWithCaptureGroupToken(): string
     75    public static function cssNestingAtRuleWithCaptureGroupToken(): string
    6776    {
    6877        $esc = self::cssEscapedString();
     
    7382
    7483        return "@(?<vendor>(?:-[^-]++-)?)(?<identifier>[a-zA-Z-]++)\s*+"
    75         . "(?<rule>(?>[^{}@/\\\\'\";\su]++|{$bc}|{$esc}|{$dqStr}|{$sqStr}|{$url}|[/u]|\s++)*?)\s*+"
    76         . "(?P<cssBlock>{(?<cssRuleList>(?>(?:[^{}/\\\\'\"]++|{$bc}|{$esc}|{$dqStr}|{$sqStr}|/)++|(?&cssBlock))*+)})";
     84            . "(?<rule>(?>[^{}@/\\\\'\";\su]++|{$bc}|{$esc}|{$dqStr}|{$sqStr}|{$url}|[/u]|\s++)*?)\s*+"
     85            . "(?P<cssBlock>{"
     86            . "(?<cssRuleList>(?>(?:[^{}/\\\\'\"]++|{$bc}|{$esc}|{$dqStr}|{$sqStr}|/)++|(?&cssBlock))*+)"
     87            . "})";
    7788    }
    7889
  • jch-optimize/trunk/lib/src/Css/CssProcessor.php

    r3402713 r3412704  
    2424use JchOptimize\Core\Css\Callbacks\HandleAtRules;
    2525use JchOptimize\Core\Css\Callbacks\PostProcessCriticalCss;
     26use JchOptimize\Core\Css\Components\CssRule;
     27use JchOptimize\Core\Css\Components\NestingAtRule;
    2628use JchOptimize\Core\Css\Sprite\Generator;
    2729use JchOptimize\Core\Exception;
     
    131133        $oParser = new Parser();
    132134        $cssRule = new CssSearchObject();
    133         $cssRule->setCssMatch(Parser::cssRuleToken());
     135        $cssRule->setCssMatch(CssRule::cssRuleWithCaptureValueToken());
    134136        $oParser->setCssSearchObject($cssRule);
    135137
    136138        $nestedAtRule = new CssSearchObject();
    137         $nestedAtRule->setCssMatch(Parser::cssNestingAtRulesToken());
     139        $nestedAtRule->setCssMatch(NestingAtRule::cssNestingAtRuleWithCaptureGroupToken());
    138140        $oParser->setCssSearchObject($nestedAtRule);
    139141
     
    261263        $oParser = new Parser();
    262264        $oCssSearchObject = new CssSearchObject();
    263         $oCssSearchObject->setCssMatch(Parser::cssRuleToken());
     265        $oCssSearchObject->setCssMatch(CssRule::cssRuleWithCaptureValueToken());
    264266        $oParser->setCssSearchObject($oCssSearchObject);
    265267
    266268        $atRuleSearchObject = new CssSearchObject();
    267         $atRuleSearchObject->setCssMatch(Parser::cssNestingAtRulesToken());
     269        $atRuleSearchObject->setCssMatch(NestingAtRule::cssNestingAtRuleWithCaptureGroupToken());
    268270        $oParser->setCssSearchObject($atRuleSearchObject);
    269271
     
    306308        $parser = new Parser();
    307309        $atRuleSearchObject = new CssSearchObject();
    308         $atRuleSearchObject->setCssMatch(Parser::cssNestingAtRulesToken());
     310        $atRuleSearchObject->setCssMatch(NestingAtRule::cssNestingAtRuleWithCaptureGroupToken());
    309311        $parser->setCssSearchObject($atRuleSearchObject);
    310312
  • jch-optimize/trunk/lib/src/Css/Parser.php

    r3310120 r3412704  
    5959
    6060        $sProcessedCss = preg_replace_callback(
    61             '#' . $regex . '#six',
     61            '#' . $regex . '#siJ',
    6262            [$callback, 'processMatches'],
    6363            $sCss
     
    8282    public function replaceMatches(string $css, string $replace): string
    8383    {
    84         $processedCss = preg_replace('#' . $this->getCssSearchRegex() . '#i', $replace, $css);
     84        $processedCss = preg_replace('#' . $this->getCssSearchRegex() . '#iJ', $replace, $css);
    8585
    8686        self::throwExceptionOnPregError();
  • jch-optimize/trunk/lib/src/Css/Xpath/CssSelector.php

    r3402713 r3412704  
    1717
    1818use function in_array;
     19use function preg_match;
    1920
    2021class CssSelector extends XpathCssSelector
     
    2324    {
    2425        return $this->type
    25                || $this->id
    26                || (int)$this->classes?->count() > 0
    27                || (int)$this->attributes?->count() > 0
    28                || (int)$this->pseudoClasses?->count() > 0
    29                || $this->pseudoElement
    30                || $this->descendant !== null;
     26            || $this->id
     27            || (int)$this->classes?->count() > 0
     28            || (int)$this->attributes?->count() > 0
     29            || (int)$this->pseudoClasses?->count() > 0
     30            || $this->pseudoElement
     31            || $this->descendant !== null;
    3132    }
    3233
     
    5960    }
    6061
    61     public function render(?string $axis = null, ?string $indexPredicate = null): string
     62    /**
     63     * Render each top-level CSS selector as XPath, optionally restricted to
     64     * the first match of each branch by appending [1].
     65     *
     66     * Pseudo-selectors inside the branch stay unchanged because we only
     67     * post-process the top-level branch string returned by CssSelector::render().
     68     */
     69    public function renderFirstPerBranch(?string $axis = null): string
    6270    {
    63         $indexPredicate = $indexPredicate ?? '[1]';
     71        // This will always be your CssSelector subclass, but keep it generic.
     72        $xpath = $this->render($axis);
    6473
    65         return parent::render() . $indexPredicate;
     74        return $this->appendFirstPredicate($xpath);
     75    }
     76
     77    /**
     78     * Append [1] to the end of the branch if it’s not already there.
     79     *
     80     * Because CssSelector::render() always returns something of the form:
     81     *   <axis>::<node>[pred][pred]...
     82     * this "[1]" applies to the whole node-set for that branch.
     83     */
     84    private function appendFirstPredicate(string $xpath): string
     85    {
     86        // Be defensive: don’t double-up if some other caller already did this.
     87        if ($xpath === '' || preg_match('/\[\s*1\s*]$/', $xpath)) {
     88            return $xpath;
     89        }
     90
     91        return $xpath . '[1]';
    6692    }
    6793}
  • jch-optimize/trunk/lib/src/Css/Xpath/PseudoClassSelector.php

    r3402713 r3412704  
    9797        return $rendered !== '' ? $rendered : "[false()]";
    9898    }
    99 
    100     /**
    101      * Critical CSS tweak on top of the base :not(...) transform:
    102      * drop a trailing [1] that can accidentally over-restrict a NOT predicate.
    103      */
    104     protected function renderNotSelectorList(): string
    105     {
    106         return $this->getSelectorList()?->render('self', '');
    107     }
    10899}
  • jch-optimize/trunk/lib/src/FileInfo.php

    r3402713 r3412704  
    4040    private ?bool $aboveFold = null;
    4141
    42     public function __construct(protected HtmlElementInterface|CssComponents $element)
    43     {
     42    public function __construct(
     43        protected HtmlElementInterface|CssComponents $element,
     44        protected ?bool $isSensitive = false
     45    ) {
    4446        $this->applyParts($element);
    4547    }
  • jch-optimize/trunk/lib/src/FileUtils.php

    r3402713 r3412704  
    6262        $path = $newUri->getPath();
    6363
     64        if ($path === '/' && ($query = $newUri->getQuery()) !== '') {
     65            $queryLen = strlen($query);
     66            if ($queryLen > $length - 1) {
     67                $query = substr($query, 0, -($queryLen - $length + 4)) . '...';
     68            }
     69
     70            $path .= '?' . $query;
     71        }
     72
    6473        if (UriComparator::isCrossOrigin($newUri)) {
    6574            $domain  = $newUri->withPort(null)->withPath('')->withQuery('')->withFragment('');
     
    102111        }
    103112
    104          return (string) $uri->withQuery('')->withFragment('');
     113        $query = '';
     114        if ($uri->getPath() === '/' && ($query = $uri->getQuery()) !== '') {
     115            $queryLen = strlen($query);
     116            $query = substr($query, 0, -($queryLen - 60));
     117        }
     118
     119        return (string) $uri->withQuery($query)->withFragment('');
    105120    }
    106121
  • jch-optimize/trunk/lib/src/Html/AttributesCollection.php

    r3402713 r3412704  
    9494        $attribute = new Attribute($name, $value, $delimiter);
    9595
    96         $this->attach($attribute);
     96        $this->offsetSet($attribute);
    9797    }
    9898
     
    136136
    137137            if ($attribute->getName() == $name) {
    138                 $this->detach($attribute);
     138                $this->offsetUnset($attribute);
    139139
    140140                return;
  • jch-optimize/trunk/lib/src/Html/BuildHtmlElement.php

    r3310120 r3412704  
    1515
    1616use JchOptimize\Core\Exception\PregErrorException;
     17use JchOptimize\Core\Exception\RuntimeException;
    1718use JchOptimize\Core\Html\Elements\BaseElement;
     19use LogicException;
    1820
    1921use function preg_match;
     
    2628    protected string $regex = '';
    2729
    28     protected BaseElement $element;
     30    protected ?BaseElement $element = null;
    2931
    3032    /**
     
    3436    {
    3537        $elementRegex = self::htmlElementWithCaptureValueToken();
    36         preg_match("#^{$elementRegex}$#s", $html, $matches);
     38        $result = preg_match("#^{$elementRegex}$#s", $html, $matches);
     39
     40        if ($result === false) {
     41            throw new RuntimeException('Failed to parse HTML string');
     42        }
     43
     44        $this->buildFromMatch($matches);
     45    }
     46
     47    /**
     48     * @throws PregErrorException
     49     */
     50    public function buildFromMatch(array $matches): void
     51    {
     52        if (!isset($matches['name'])) {
     53            $this->build($matches[0]);
     54
     55            return;
     56        }
    3757
    3858        $name = strtolower($matches['name']);
    3959        $this->element = HtmlElementBuilder::$name();
    4060
     61        $attrsText = $matches['attributes'] ?? '';
     62        if ($attrsText !== '') {
     63            $this->loadAttributesFromText($attrsText);
     64        }
     65
     66        if (!empty($matches['content'])) {
     67            $this->loadChildren($matches['content']);
     68        }
     69
     70        if (empty($matches['endTag'])) {
     71            $this->element->setOmitClosingTag(true);
     72        }
     73    }
     74
     75    private function loadAttributesFromText(string $attributesText): void
     76    {
    4177        $attributesRegex = self::htmlAttributeWithCaptureValueToken();
     78
    4279        preg_match_all(
    4380            '#' . $attributesRegex . '#ix',
    44             $matches['attributes'] ?? '',
     81            $attributesText,
    4582            $attributes,
    4683            PREG_SET_ORDER
     
    5289            $delimiter = $attribute['delimiter'] ?? '"';
    5390
    54             $this->element->attribute($name, $value, $delimiter);
    55         }
    56 
    57         if (isset($matches['content'])) {
    58             $this->loadChildren($matches['content']);
    59         } else {
    60             $this->element->setOmitClosingTag(true);
     91            $this->element?->attribute($name, $value, $delimiter);
    6192        }
    6293    }
    6394
    64     public function getElement(): HtmlElementInterface
     95
     96    public function getElement(): BaseElement
    6597    {
     98        if ($this->element === null) {
     99            throw new LogicException('Element not set');
     100        }
     101
    66102        return $this->element;
    67103    }
     
    73109        $endTag = Parser::htmlEndTagToken(Parser::htmlGenericElementNameToken());
    74110
    75         return "<(?<name>{$name})\b(\s++(?<attributes>{$attributes}+))?/?>(?:(?<content>.*){$endTag})?";
     111        return "<(?<name>{$name})\b(?:\s++(?<attributes>{$attributes}+))?/?>(?:(?<content>.*)(?<endTag>{$endTag}))?";
    76112    }
    77113
     
    104140        $lc = Parser::lineCommentToken();
    105141        //Regular expression literal
    106         $rx =  '/(?![/*])(?>(?(?=\\\\)\\\\.|\[(?>(?:\\\\.)?[^\]\r\n]*+)+?\])?[^\\\\/\r\n\[]*+)+?/';
     142        $rx = '/(?![/*])(?>(?(?=\\\\)\\\\.|\[(?>(?:\\\\.)?[^\]\r\n]*+)+?\])?[^\\\\/\r\n\[]*+)+?/';
    107143
    108144        $htmlElementRegex = "(?:{$voidElement}|{$textElement})";
    109145        $regex = "(?<string>(?>[^<'\"/`]++|{$bc}|{$lc}|{$rx}|{$dqStr}|{$sqStr}|{$btStr}|/|(?!{$htmlElementRegex})<)++)"
    110         . "|(?<element>(?:{$voidElement}|{$textElementMatch}))";
     146            . "|(?<element>(?:{$voidElement}|{$textElementMatch}))";
    111147
    112148        preg_match_all(
     
    118154
    119155        foreach ($matches as $match) {
    120             if (!empty($match['element'])) {
     156            if (isset($match['element'])) {
    121157                $child = HtmlElementBuilder::load($match['element']);
    122                 $child->setParent($this->element->getElementName());
    123                 $this->element->addChild($child);
    124             } elseif (!empty($match['string'])) {
    125                 $this->element->addChild($match['string']);
     158                $child->setParent($this->getElement()->getElementName());
     159                $this->getElement()->addChild($child);
     160            } elseif (isset($match['string'])) {
     161                $this->getElement()->addChild($match['string']);
    126162            }
    127163        }
  • jch-optimize/trunk/lib/src/Html/CacheManager.php

    r3402713 r3412704  
    130130        $combinedByGroup = [];
    131131        $cacheObjByGroup = [];
     132        $sensitiveCacheObjByOrds = [];
    132133        $belowFoldFontsEl = null;
    133134        $reducedBundleEl = null;
     
    136137        $reduceUnusedCss = $this->params->get('pro_reduce_unused_css', '0');
    137138
    138         $layoutPlanner = $this->cssLayoutPlanner->plan(
    139             $this->filesManager->cssTimeLine,
    140             $optimizeDelivery,
    141             $reduceUnusedCss
    142         );
     139        $cssTimeLine = $this->filesManager->cssTimeLine;
     140        $layoutPlanner = $this->cssLayoutPlanner->plan($cssTimeLine, $optimizeDelivery, $reduceUnusedCss);
     141
     142        if ($optimizeDelivery) {
     143            foreach ($cssTimeLine as $item) {
     144                if ($item->isSensitive) {
     145                    $fileInfo = new FileInfo(clone $item->node);
     146                    $cacheObj = $this->getCombinedFiles([$fileInfo], $id, 'css', false, $hit);
     147                    $this->updateOptimizeCssDelivery($cacheObj, $item->node, $hit, false);
     148                    $sensitiveCacheObjByOrds[$item->ordinal] = $cacheObj;
     149                }
     150            }
     151        }
    143152
    144153        /**
    145          * @var  int        $cssLinksKey
     154         * @var  int $cssLinksKey
    146155         * @var  FileInfo[] $cssInfosArray
    147156         */
     
    195204            $combinedByGroup,
    196205            $cacheObjByGroup,
     206            $sensitiveCacheObjByOrds,
    197207            $belowFoldFontsEl,
    198208            $reducedBundleEl
     
    214224        $combinedByGroup = [];
    215225        /**
    216          * @var int        $aJsLinksKey
     226         * @var int $aJsLinksKey
    217227         * @var FileInfo[] $jsInfosArray
    218228         */
     
    278288
    279289            return $results;
    280         } catch (Exception|LaminasCacheExceptionInterface $e) {
     290        } catch (Exception | LaminasCacheExceptionInterface $e) {
    281291            throw new RuntimeException('Error creating cache files: ' . $e->getMessage());
    282292        }
     
    374384
    375385                    if (
    376                         in_array($name, ['id', 'integrity', 'crossorigin', 'referrerpolicy', 'nonce'])
     386                        in_array($name, ['id', 'class', 'nonce'])
    377387                        || (str_starts_with('data-', $name))
    378388                    ) {
     
    472482            if ($criticalCssAlreadyExisted) {
    473483                $this->getContainer()->get(CriticalCssDependencies::class)
    474                      ->addToCriticalCssAggregate($criticalCssObj->getCriticalCss())
    475                      ->addToDynamicCriticalCssAggregate($criticalCssObj->getDynamicCriticalCss())
    476                      ->addToPotentialCriticalCssAtRules($criticalCssObj->getPotentialCriticalCssAtRules());
     484                    ->addToCriticalCssAggregate($criticalCssObj->getCriticalCss())
     485                    ->addToDynamicCriticalCssAggregate($criticalCssObj->getDynamicCriticalCss())
     486                    ->addToPotentialCriticalCssAtRules($criticalCssObj->getPotentialCriticalCssAtRules());
    477487            }
    478488
     
    496506        $cssProcessor->setCssInfos(new FileInfo($element));
    497507        $this->getContainer()->get(CriticalCssDependencies::class)
    498              ->addToPotentialCriticalCssAtRules($cssProcessor->getCacheObj()->getPotentialCriticalCssAtRules());
     508            ->addToPotentialCriticalCssAtRules($cssProcessor->getCacheObj()->getPotentialCriticalCssAtRules());
    499509        $cssProcessor->optimizeCssDelivery();
    500510
     
    516526    {
    517527        $style = HtmlElementBuilder::style()
    518                                    ->addChild($cssCacheObj->getBelowFoldFontsKeyFrame());
     528            ->addChild($cssCacheObj->getBelowFoldFontsKeyFrame());
    519529        $fileInfo = new FileInfo($style);
    520530        $fileInfo->setAlreadyProcessed(true);
  • jch-optimize/trunk/lib/src/Html/Callbacks/AbstractCallback.php

    r3310120 r3412704  
    5858
    5959        try {
    60             $element = HtmlElementBuilder::load($matches[0]);
     60            $element = HtmlElementBuilder::loadFromMatch($matches);
    6161        } catch (PregErrorException) {
    6262            return $matches[0];
  • jch-optimize/trunk/lib/src/Html/Callbacks/CombineJsCss.php

    r3402713 r3412704  
    178178        if ($element instanceof Script && $element->hasAttribute('src')) {
    179179            if (Helper::uriInvalid($element->getSrc())) {
    180                 return $element->render();
     180                return '';
    181181            }
    182182        }
     
    184184        if ($element instanceof Link && $element->hasAttribute('href')) {
    185185            if (Helper::uriInvalid($element->getHref())) {
    186                 return $element->render();
     186                return '';
    187187            }
    188188        }
  • jch-optimize/trunk/lib/src/Html/CssLayout/CssItem.php

    r3402713 r3412704  
    2626        public bool $isInline,      // <style> vs <link>
    2727        public bool $isMarker,
     28        public bool $isSensitive,
    2829        public ?string $media,      // media attr, if any
    2930        public Link|Style|null $node     // original element
  • jch-optimize/trunk/lib/src/Html/CssLayout/CssLayoutPlanner.php

    r3402713 r3412704  
    3434                isMarker: $item->isMarker,
    3535                groupIndex: $item->groupIndex,
     36                isSensitive: $item->isSensitive,
    3637                item: $item,
    3738            );
    3839
     40            // 1) Head-blocking list
    3941            // IEO / removed CSS is handled by FilesManager and never enters the plan.
    4042            // Excluded (non-processed) CSS still participates in "headBlocking" layout.
    4143            if ((!$optimizeDelivery && !$reduceUnusedCss) || $item->isExcluded) {
    4244                $plan->headBlocking[] = $placement;
    43             } elseif ($optimizeDelivery && !$reduceUnusedCss && $item->isProcessed) {
     45
     46                continue;
     47            }
     48
     49            // 2) Sensitive CSS handling
     50            if ($item->isSensitive) {
     51                // Optimize OFF: they just behave as head-blocking CSS already (above).
     52                if ($optimizeDelivery) {
     53                    // Optimize Delivery ON:
     54                    // - use them for critical & dynamic-critical
     55                    $plan->headInlineCritical[] = $placement;
     56                    $plan->bodyInlineDynamicCritical[] = $placement;
     57
     58                    // Reduce-unused OFF: also load the original file asynchronously.
     59                    if (!$reduceUnusedCss) {
     60                        $plan->bodyAsync[] = $placement;
     61                    } else {
     62                        $plan->bodySensitiveDynamic[] = $placement;
     63                    }
     64                }
     65
     66                // Don't fall through into "processed group" logic
     67                continue;
     68            }
     69
     70            // 3) Normal (non-sensitive) CSS handling
     71            if ($optimizeDelivery && !$reduceUnusedCss && $item->isProcessed) {
    4472                // Optimize CSS Delivery ON, Reduce Unused OFF:
    4573                // - critical CSS in head
  • jch-optimize/trunk/lib/src/Html/CssLayout/CssPlacementItem.php

    r3402713 r3412704  
    2020        public bool $isMarker,
    2121        public ?int $groupIndex,     // group index in aCss
     22        public bool $isSensitive,
    2223        public ?CssItem $item     // original element (for inline/excluded cases)
    2324    ) {
  • jch-optimize/trunk/lib/src/Html/CssLayout/CssPlacementPlan.php

    r3402713 r3412704  
    4646    public array $bodyInlineDynamicCritical = [];
    4747
    48         /**
     48    /**
     49     * Sensitive items that should be loaded dynamically
     50     *
     51     * @var CssPlacementItem[];
     52     */
     53    public array $bodySensitiveDynamic = [];
     54
     55    /**
    4956     * Whether we should append a “below the fold fonts” CSS block in the body.
    5057     * The actual element is created in CacheManager and passed to HtmlManager.
  • jch-optimize/trunk/lib/src/Html/Elements/BaseElement.php

    r3402713 r3412704  
    265265
    266266            $newAttribute = new Attribute($name, $value, $delimiter);
    267             $attributes->attach($newAttribute);
     267            $attributes->offsetSet($newAttribute);
    268268        }
    269269
  • jch-optimize/trunk/lib/src/Html/FilesManager.php

    r3402713 r3412704  
    7878
    7979    /**
     80     * @var bool Flagged when a sensitive CSS file is encountered
     81     */
     82    protected bool $cssSensitive = false;
     83
     84    /**
     85     * @var bool Flagged when a sensitive JS file is encountered
     86     */
     87    protected bool $jsSensitive = false;
     88    /**
    8089     * @var array $aCss Multidimensional array of css files to combine
    8190     */
     
    256265        }
    257266
    258         if ($this->isDuplicated($uri)) {
    259             $this->replacement = '';
    260             $this->excludeCssIEO();
     267        if ($this->isSensitiveExternal($link, $uri)) {
     268            $this->recordSensitiveCss($link, $media);
     269
     270            // No PEO/IEO excludes, no adding to $aCss; planner will later:
     271            // - use it for critical CSS extraction
     272            // - schedule original href async/dynamic
     273            return;
    261274        }
    262275
     
    340353    private function excludeCssPEO(Link|Style $element, ?string $media = null)
    341354    {
    342         //if previous file was not excluded increment css index
    343         if (!$this->cssExcludedPeo && !empty($this->cssReplacements[0])) {
    344             $this->iIndex_css++;
    345         }
    346 
    347355        $this->cssExcludedPeo = true;
    348356        $this->sCssExcludeType = 'peo';
     
    357365            isInline: $element instanceof Style,
    358366            isMarker: $ordinal === 0,
     367            isSensitive: false,
    359368            media: $media,
    360369            node: $element
     
    386395    private function updateIndex(): void
    387396    {
    388         if (!$this->params->get('combine_files', '0')) {
    389             $element = $this->getElement();
    390 
    391             if ($element instanceof Script) {
    392                 // Don't increase index if we're in an exclude. Index already incremented.
    393                 if (!$this->jsExcludedPeo && !empty($this->aJs[$this->iIndex_js] ?? [])) {
    394                     $this->iIndex_js++;
    395                 }
    396             } elseif ($element instanceof Link || $element instanceof Style) {
    397                 // Same logic for CSS
    398                 if (!$this->cssExcludedPeo && !empty($this->aCss[$this->iIndex_css] ?? [])) {
    399                     $this->iIndex_css++;
    400                 }
     397        $combineFiles = $this->params->get('combine_files', '0');
     398        $element = $this->getElement();
     399
     400        if ($element instanceof Script && !empty($this->aJs[$this->iIndex_js] ?? [])) {
     401            if (!$combineFiles) {
     402                $this->iIndex_js++;
     403            } elseif ($this->jsExcludedPeo || $this->jsSensitive) {
     404                $this->iIndex_js++;
     405            }
     406        } elseif (
     407            ($element instanceof Link || $element instanceof Style)
     408            && !empty($this->aCss[$this->iIndex_css] ?? [])
     409        ) {
     410            // Same logic for CSS
     411            if (!$combineFiles) {
     412                $this->iIndex_css++;
     413            } elseif ($this->cssExcludedPeo || $this->cssSensitive) {
     414                $this->iIndex_css++;
    401415            }
    402416        }
     
    439453        if ($this->isDuplicated($uri)) {
    440454            $this->replacement = '';
    441             $this->excludeJsIEO($script);
     455            return;
     456        }
     457
     458        // --- sensitivity check for external JS ---
     459        if ($this->isSensitiveExternal($script, $uri)) {
     460            $this->recordSensitiveJs($script);
     461
     462            // We don't process or exclude it; just keep it in the timeline for the planner.
     463            return;
    442464        }
    443465
     
    483505            isIeo: true,
    484506            isDeferred: Helper::isScriptDeferred($script),
     507            isSensitive: false,
    485508            node: $script
    486509        );
     
    506529            isIeo: false,
    507530            isDeferred: Helper::isScriptDeferred($script),
     531            isSensitive: false,
    508532            node: $script
    509533        );
     
    518542    {
    519543        $content = $script->getChildren()[0];
     544
     545        //Exclude all scripts if options set
     546        if (
     547            !$this->params->get('inlineScripts', '0')
     548            || $this->params->get('excludeAllScripts', '0')
     549        ) {
     550            $this->excludeJsPEO($script);
     551        }
    520552
    521553        if ($this->sectionExcludes !== null) {
     
    536568        }
    537569
    538         //Exclude all scripts if options set
    539         if (
    540             !$this->params->get('inlineScripts', '0')
    541             || $this->params->get('excludeAllScripts', '0')
    542         ) {
    543             $this->excludeJsPEO($script);
    544         }
    545 
    546570        $this->maybeProcessScript($script);
    547     }
    548 
    549     private function responseToPreviousExclude(): void
    550     {
    551         //If previous file was excluded PEO, update index
    552         if ($this->jsExcludedPeo) {
    553             $this->iIndex_js++;
    554         }
    555571    }
    556572
     
    572588        if (!$isDeferred) {
    573589            $this->updateIndex();
    574             $this->responseToPreviousExclude();
    575             $this->jsExcludedPeo = false;
    576             $this->jsExcludedIeo = false;
    577590            $this->aJs[$this->iIndex_js][] = new FileInfo(clone $script);
    578591            $this->jsReplacements[$this->iIndex_js][] = $script;
     
    580593            $groupIndex = $this->iIndex_js;
    581594        }
     595
     596        //These properties must only be set after index is (maybe) updated
     597        $this->jsExcludedPeo = false;
     598        $this->jsExcludedIeo = false;
     599        $this->cssSensitive = false;
    582600
    583601        $existingGroupIndexes = array_filter(
     
    595613                isIeo: false,
    596614                isDeferred: $isDeferred,
     615                isSensitive: false,
    597616                node: !$this->params->get('combine_files', 0) ? clone $script : null
    598617            );
     
    603622    {
    604623        $this->updateIndex();
     624        //These properties must be set only after index is updated
    605625        $this->cssExcludedPeo = false;
    606626        $this->cssExcludedIeo = false;
     627        $this->cssSensitive = false;
     628
    607629        $this->aCss[$this->iIndex_css][] = new FileInfo(clone $element);
    608630        $this->cssReplacements[$this->iIndex_css][] = $element;
     
    623645                isInline: false,
    624646                isMarker: $ordinal === 0,
     647                isSensitive: false,
    625648                media: $media,
    626649                node: !$this->params->get('combine_files', 0) ? clone $element : null
     
    628651        }
    629652    }
     653
     654    private function hasSensitiveAttributes(HtmlElementInterface $element): bool
     655    {
     656        return $element->hasAttribute('integrity')
     657            || $element->hasAttribute('crossorigin')
     658            || $element->hasAttribute('referrerpolicy');
     659    }
     660
     661    private function isExternal(UriInterface $uri): bool
     662    {
     663        $resolvedUri = UriResolver::resolve(SystemUri::currentUri(), $uri);
     664
     665        $cdn = $this->getContainer()->get(Cdn::class);
     666        $path = $this->getContainer()->get(PathsInterface::class);
     667
     668        return !UriComparator::existsLocally($resolvedUri, $cdn, $path);
     669    }
     670
     671    private function isSensitiveExternal(HtmlElementInterface $element, UriInterface $uri): bool
     672    {
     673        return $this->hasSensitiveAttributes($element) && $this->isExternal($uri);
     674    }
     675
     676    private function recordSensitiveJs(Script $script): void
     677    {
     678        $this->jsSensitive = true;
     679        $this->replacement = '';
     680
     681        $this->jsTimeLine[] = new JsItem(
     682            ordinal: count($this->jsTimeLine),
     683            groupIndex: null,               // not part of any processed group
     684            isProcessed: false,
     685            isExcluded: false,
     686            isGate: false,
     687            isIeo: false,
     688            isDeferred: Helper::isScriptDeferred($script),
     689            isSensitive: true,
     690            node: clone $script
     691        );
     692    }
     693
     694    private function recordSensitiveCss(Link $link, ?string $media): void
     695    {
     696        $this->cssSensitive = true;
     697        $this->replacement = '';
     698        $ordinal = count($this->cssTimeLine);
     699
     700        $this->cssTimeLine[] = new CssItem(
     701            ordinal: $ordinal,
     702            groupIndex: null,               // not part of any processed group
     703            isProcessed: false,
     704            isExcluded: false,
     705            isInline: false,
     706            isMarker: $ordinal === 0,
     707            isSensitive: true,
     708            media: $media,
     709            node: clone $link
     710        );
     711    }
    630712}
  • jch-optimize/trunk/lib/src/Html/HtmlElementBuilder.php

    r3310120 r3412704  
    8282    {
    8383        $builder = new BuildHtmlElement();
     84        $builder->build($html);
    8485
    85         $builder->build($html);
     86        return $builder->getElement();
     87    }
     88
     89    /**
     90     * @throws PregErrorException
     91     */
     92    public static function loadFromMatch(array $matches): HtmlElementInterface
     93    {
     94        $builder = new BuildHtmlElement();
     95        $builder->buildFromMatch($matches);
    8696
    8797        return $builder->getElement();
  • jch-optimize/trunk/lib/src/Html/HtmlManager.php

    r3402713 r3412704  
    107107    {
    108108        return HtmlElementBuilder::style()
    109                                  ->class('jchoptimize-critical-css')
    110                                  ->data('id', $id)
    111                                  ->addChild(PHP_EOL . $criticalCss . PHP_EOL)
    112                                  ->render();
     109            ->class('jchoptimize-critical-css')
     110            ->data('id', $id)
     111            ->addChild(PHP_EOL . $criticalCss . PHP_EOL)
     112            ->render();
    113113    }
    114114
     
    211211    {
    212212        return ($this->params->get('gzip', 0) && extension_loaded('zlib') && !ini_get('zlib.output_compression')
    213                 && (ini_get('output_handler') != 'ob_gzhandler'));
     213            && (ini_get('output_handler') != 'ob_gzhandler'));
    214214    }
    215215
     
    263263        if ($element instanceof Link) {
    264264            $attr = [
    265                 'rel'    => 'preload',
    266                 'as'     => 'style',
     265                'rel' => 'preload',
     266                'as' => 'style',
    267267                'onload' => 'this.rel=\'stylesheet\'',
    268268            ];
     
    271271            $attr = [
    272272                'onload' => "this.media='{$media}'",
    273                 'media'  => 'print'
     273                'media' => 'print'
    274274            ];
    275275        }
     
    290290    {
    291291        return HtmlElementBuilder::style()
    292                                  ->class('jchoptimize-dynamic-critical-css')
    293                                  ->data('id', $id)
    294                                  ->addChild(PHP_EOL . $css . PHP_EOL)
    295                                  ->render();
     292            ->class('jchoptimize-dynamic-critical-css')
     293            ->data('id', $id)
     294            ->addChild(PHP_EOL . $css . PHP_EOL)
     295            ->render();
    296296    }
    297297
    298298    /**
    299      * @param   CssPlacementPlan  $plan
    300      * @param   (Link|Style)[]    $combinedByGroup
    301      * @param   CacheObject[]     $cacheObjectByGroup
    302      * @param   Link|null         $belowFoldFontsEl
    303      * @param   Link|null         $reducedBundleEl
     299     * @param CssPlacementPlan $plan
     300     * @param (Link|Style)[] $combinedByGroup
     301     * @param CacheObject[] $cacheObjectByGroup
     302     * @param CacheObject[] $sensitiveCacheObjByOrds
     303     * @param Link|null $belowFoldFontsEl
     304     * @param Link|null $reducedBundleEl
    304305     *
    305306     * @return void
     
    309310        array $combinedByGroup,
    310311        array $cacheObjectByGroup,
     312        array $sensitiveCacheObjByOrds,
    311313        ?Link $belowFoldFontsEl,
    312314        ?Link $reducedBundleEl
     
    372374
    373375            foreach ($plan->headInlineCritical as $placement) {
     376                if ($placement->isSensitive) {
     377                    $sCacheObj = $sensitiveCacheObjByOrds[$placement->item->ordinal] ?? null;
     378                    if ($sCacheObj !== null && ($sCss = $sCacheObj->getCriticalCss()) !== '') {
     379                        $insertion .= "\t" . $this->getCriticalCssHtml($sCss, $sCacheObj->getCriticalCssId()) . PHP_EOL;
     380                    }
     381                    continue;
     382                }
     383
    374384                $cacheObj = $cacheObjectByGroup[$placement->groupIndex] ?? null;
    375385                if ($cacheObj !== null && ($css = $cacheObj->getCriticalCss()) !== '') {
     
    386396
    387397            foreach ($plan->bodyInlineDynamicCritical as $placement) {
     398                if ($placement->isSensitive) {
     399                    $sCacheObj = $sensitiveCacheObjByOrds[$placement->item->ordinal] ?? null;
     400                    if ($sCacheObj !== null && ($sCss = $sCacheObj->getDynamicCriticalCss()) !== '') {
     401                        $insertion .= "\t" . $this->getDynamicCriticalCssHtml(
     402                            $sCss,
     403                            $sCacheObj->getCriticalCssId()
     404                        ) . PHP_EOL;
     405                    }
     406                    continue;
     407                }
     408
    388409                $cacheObj = $cacheObjectByGroup[$placement->groupIndex] ?? null;
    389410                if ($cacheObj !== null && ($css = $cacheObj->getDynamicCriticalCss()) !== '') {
    390411                    $insertion .= "\t" . $this->getDynamicCriticalCssHtml(
    391                             $css,
    392                             $cacheObj->getCriticalCssId()
    393                         ) . PHP_EOL;
     412                        $css,
     413                        $cacheObj->getCriticalCssId()
     414                    ) . PHP_EOL;
    394415                }
    395416            }
     
    408429
    409430            foreach ($plan->bodyAsync as $placement) {
     431                if ($placement->isSensitive) {
     432                    $el = $placement->item->node;
     433                    $this->preloadStyleSheet($el, 'low');
     434                    $insertion .= "\t" . $el->render() . PHP_EOL;
     435
     436                    continue;
     437                }
     438
    410439                $cssEl = $combinedByGroup[$placement->groupIndex] ?? null;
    411440                if ($cssEl) {
     
    442471        }
    443472
     473        if (!empty($plan->bodySensitiveDynamic)) {
     474            $insertion = '';
     475
     476            foreach ($plan->bodySensitiveDynamic as $placement) {
     477                $el = $placement->item->node->type('jchoptimize-text/css');
     478                $insertion .= "\t" . $el->render() . PHP_EOL;
     479            }
     480
     481            $html = preg_replace(
     482                '#' . Parser::htmlClosingBodyTagToken() . '#si',
     483                $insertion . '\0',
     484                $html,
     485                1
     486            );
     487        }
     488
    444489        $this->processor->setFullHtml($html);
    445490    }
     
    512557                    JCH_PRO
    513558                    && $this->params->get('pro_reduce_unused_js_enable', '0')
    514                     && (
    515                         ($section === 'body' && $placement->isProcessed && $placement->isDeferable)
    516                         || ($placement->isDeferred && !$placement->isExcluded)
    517                     )
     559                    && $section === 'body'
     560                    && ($placement->isDeferable || $placement->isDeferred)
     561                    && !$placement->isExcluded
    518562                    && $dynamicJs instanceof DynamicJs
    519563                ) {
     
    522566                        $insertion .= "\t" . (string)$script . PHP_EOL;
    523567                    }
     568                } elseif (
     569                    $this->params->get('loadAsynchronous', '0')
     570                    && $section === 'body'
     571                    && $placement->isDeferable && !$placement->isExcluded
     572                ) {
     573                    if ($placement->isProcessed) {
     574                        $script = $combinedByGroup[$placement->groupIndex] ?? null;
     575                    } else {
     576                        $script = $placement->item->node;
     577                    }
     578
     579                    if ($script) {
     580                        // Add defer/async if loadAsynchronous & bottom_js logic apply
     581                        $this->deferScript($script);
     582                        $insertion .= "\t" . $script->render() . PHP_EOL;
     583                    }
    524584                } elseif ($placement->isProcessed) {
    525585                    $script = $combinedByGroup[$placement->groupIndex] ?? null;
    526586                    if ($script) {
    527                         // Add defer/async if loadAsynchronous & bottom_js logic apply
    528                         if (
    529                             $section === 'body'
    530                             && $this->params->get('loadAsynchronous', '0')
    531                             && $placement->isDeferable
    532                         ) {
    533                             $this->deferScript($script);
    534                         }
    535587                        $insertion .= "\t" . $script->render() . PHP_EOL;
    536588                    }
     
    577629    {
    578630        return HtmlElementBuilder::link()
    579                                  ->rel('modulepreload')
    580                                  ->href($url)
    581                                  ->fetchpriority('low')
    582                                  ->render();
     631            ->rel('modulepreload')
     632            ->href($url)
     633            ->fetchpriority('low')
     634            ->render();
    583635    }
    584636
     
    643695        if ($css !== '') {
    644696            $style = HtmlElementBuilder::style()
    645                                        ->id('jchoptimize-custom-css')
    646                                        ->addChild($css)
    647                                        ->render();
     697                ->id('jchoptimize-custom-css')
     698                ->addChild($css)
     699                ->render();
    648700
    649701            $this->appendChildToHead($style);
     
    666718     * @template T of Link|Script
    667719     *
    668      * @param   T         $element  The HTML element to add the data-file attribute to.
    669      * @param   FileInfo  $fileInfo The file information.
     720     * @param T $element The HTML element to add the data-file attribute to.
     721     * @param FileInfo $fileInfo The file information.
    670722     *
    671723     * @return T Returns the same type as the $element input (Link or Script).
  • jch-optimize/trunk/lib/src/Html/HtmlProcessor.php

    r3402713 r3412704  
    3232use JchOptimize\Core\Html\Elements\Img;
    3333use JchOptimize\Core\Html\Elements\Source;
     34use JchOptimize\Core\Optimize;
    3435use JchOptimize\Core\Platform\ProfilerInterface;
    3536use JchOptimize\Core\Registry;
     
    316317            !JCH_DEBUG ?: $this->profiler->start('LazyLoadImages');
    317318
    318             $html = $this->getBodyHtml();
    319 
    320             $aboveFoldBody = $this->getAboveFoldHtml($html);
    321             $belowFoldHtml = substr($html, strlen($aboveFoldBody));
    322             $fullHtml = $this->getFullHtml();
    323             $aboveFoldHtml = substr($fullHtml, 0, strlen($fullHtml) - strlen($belowFoldHtml)) . $this->regexMarker;
     319            $bodyHtml = $this->getBodyHtml();
     320
     321            $aboveFoldBody = $this->getAboveFoldHtml($bodyHtml);
     322            $aboveFoldBodyLen = strlen($aboveFoldBody);
     323            $belowFoldBody = substr($bodyHtml, $aboveFoldBodyLen);
     324
     325            $headHtml = $this->getHeadHtml();
    324326
    325327            try {
     
    336338                $http2Callback = $this->getContainer()->get(LazyLoad::class);
    337339                $http2Callback->setLazyLoadArgs($http2Args);
    338                 $processedAboveFoldHtml = $aboveFoldParser->processMatchesWithCallback(
    339                     $aboveFoldHtml,
    340                     $http2Callback
    341                 );
     340                $processedHeadHtml = $aboveFoldParser->processMatchesWithCallback($headHtml, $http2Callback);
     341                $this->setHeadHtml($processedHeadHtml);
     342                $processedAboveFoldBody = $aboveFoldParser->processMatchesWithCallback($aboveFoldBody, $http2Callback);
    342343
    343344                $belowFoldParser = new Parser();
     
    353354                $lazyLoadCallback->setLazyLoadArgs($lazyLoadArgs);
    354355                $processedBelowFoldHtml = $belowFoldParser->processMatchesWithCallback(
    355                     $belowFoldHtml,
     356                    $belowFoldBody,
    356357                    $lazyLoadCallback
    357358                );
     
    366367                }
    367368
    368                 $this->setFullHtml(
    369                     $this->cleanRegexMarker($processedAboveFoldHtml) . $marker . $processedBelowFoldHtml
     369                $this->setBodyHtml(
     370                    $this->cleanRegexMarker($processedAboveFoldBody) . $marker . $processedBelowFoldHtml
    370371                );
    371372            } catch (PregErrorException $oException) {
     
    443444        if (
    444445            !$this->params->get('cookielessdomain_enable', '0') ||
    445             (trim($this->params->get('cookielessdomain', '')) == '' &&
     446            (
     447                trim($this->params->get('cookielessdomain', '')) == '' &&
    446448                trim($this->params->get('pro_cookielessdomain_2', '')) == '' &&
    447                 trim($this->params->get('pro_cookieless_3', '')) == '')
     449                trim($this->params->get('pro_cookieless_3', '')) == ''
     450            )
    448451        ) {
    449452            return;
     
    687690        } catch (PregErrorException $e) {
    688691            $this->logger?->error('RemoveScriptsFromHtml failed ' . $e->getMessage());
     692
    689693            return $html;
    690694        }
     
    693697    public function processJavaScriptForConfigureHelper(): array
    694698    {
     699        Optimize::setPcreLimits();
    695700        $dynamicScripts = [];
    696701
  • jch-optimize/trunk/lib/src/Html/JsLayout/JsItem.php

    r3402713 r3412704  
    2626        public bool $isIeo,       // ignore execution order (IEO)
    2727        public bool $isDeferred,  // deferred/module etc
     28        public bool $isSensitive,
    2829        public ?Script $node       // HTML element for this script
    2930    ) {
  • jch-optimize/trunk/lib/src/Html/JsLayout/JsLayoutPlanner.php

    r3402713 r3412704  
    5757            $placement = new JsPlacementItem(
    5858                isProcessed: $item->isProcessed,
    59                 isDeferable: $item->isProcessed && $item->ordinal > $lastPEOExcludeOrdinal,
     59                isDeferable: ($item->isProcessed || $item->isSensitive) && $item->ordinal > $lastPEOExcludeOrdinal,
    6060                isDeferred: $item->isDeferred,
    6161                isExcluded: $item->isExcluded,
     62                isSensitive: $item->isSensitive,
    6263                groupIndex: $item->groupIndex,
    6364                item: $item
  • jch-optimize/trunk/lib/src/Html/JsLayout/JsPlacementItem.php

    r3402713 r3412704  
    2121        public bool $isDeferred,
    2222        public bool $isExcluded,
     23        public bool $isSensitive,
    2324        public ?int $groupIndex, //For processed items
    2425        public JsItem $item
  • jch-optimize/trunk/lib/src/Html/Parser.php

    r3310120 r3412704  
    2727use function preg_replace;
    2828use function preg_replace_callback;
    29 use function str_contains;
    3029
    3130defined('_JCH_EXEC') or die('Restricted access');
     
    5049    public static function htmlBodyElementToken(): string
    5150    {
    52         $htmlHead = self::htmlHeadElementToken();
    53 
    54         return "{$htmlHead}\K.*+$";
     51        $bodyStartTag = self::htmlStartTagToken('body');
     52        $htmlString = self::htmlStringToken(['script', 'style', 'template']);
     53        $bodyEndTag = self::htmlEndTagToken('body');
     54
     55        return "{$bodyStartTag}{$htmlString}?{$bodyEndTag}";
    5556    }
    5657
     
    108109
    109110        $sProcessedHtml = (string)preg_replace_callback(
    110             '#' . $regex . '#si',
     111            '#' . $regex . '#siJ',
    111112            [$callbackObject, 'processMatches'],
    112113            $html
     
    209210    {
    210211        $regex = $this->getHtmlSearchRegex();
    211         preg_match_all('#' . $regex . '#si', $sHtml, $aMatches, $flags);
     212        preg_match_all('#' . $regex . '#siJ', $sHtml, $aMatches, $flags);
    212213
    213214        self::throwExceptionOnPregError();
     
    233234    {
    234235        $regex = $this->getHtmlSearchRegex();
    235         $result = preg_replace("#{$regex}#si", "", $html);
     236        $result = preg_replace("#{$regex}#siJ", "", $html);
    236237
    237238        self::throwExceptionOnPregError();
     
    246247
    247248        foreach ($attrCriteria as $criterion) {
    248             if (str_contains($criterion, '!=')) {
    249                 list($name, $value) = explode('!=', $criterion);
    250                 $criterionRegexArray[] = "{$name}\s*+=\s*+(?!\"{$value}\"|'{$value}'|(?<=[=])(?!['\"]){$value}[\s/> ])";
    251             } elseif (str_contains($criterion, '==')) {
    252                 list($name, $value) = explode('==', $criterion);
    253 
    254                 $criterionRegexArray[] = "$name\s*+=\s*+(?:\"{$value}\"|'{$value}'|(?<=[=])(?!['\"]){$value}[\s/> ])";
    255             } elseif (str_contains($criterion, '~=')) {
    256                 list($name, $value) = explode('~=', $criterion);
    257 
    258                 $criterionRegexArray[] = "{$name}\s*+=\s*+(?:\"[^\"]*?(?<=[\" ]){$value}[\" ]|"
    259                     . "'[^']*?(?<=[' ]){$value}[ ']|(?<==)(?!['\"]){$value}[ />])";
    260             } elseif (str_contains($criterion, '*=')) {
    261                 list($name, $value) = explode('*=', $criterion);
    262 
    263                 $criterionRegexArray[] = "{$name}\s*+=\s*+(?:\"[^\"]*?{$value}|'[^']*?{$value}|(?<==)(?!['\"])[^\s]*?{$value})";
    264             } else {
    265                 $criterionRegexArray[] = $criterion;
    266             }
     249            $criterionRegexArray[] = $this->buildCriterionRegex($criterion);
    267250        }
    268251
     
    270253
    271254        return "(?:$criterionRegexString)";
     255    }
     256
     257    private function buildCriterionRegex(string $criterion): string
     258    {
     259        // Find the operator once
     260        if (!preg_match('#(==|!=|~=|\*=)#', $criterion, $m)) {
     261            // No operator we care about → return as-is
     262            return $criterion;
     263        }
     264
     265        $op = $m[1];
     266
     267        // Split on the operator (only once)
     268        [$name, $value] = explode($op, $criterion, 2);
     269
     270        return match ($op) {
     271            '!=' => $this->buildNotEqualsRegex($name, $value),
     272            '==' => $this->buildEqualsRegex($name, $value),
     273            '~=' => $this->buildContainsWordRegex($name, $value),
     274            '*=' => $this->buildContainsSubstringRegex($name, $value),
     275        };
     276    }
     277
     278    private function buildNotEqualsRegex(string $name, string $value): string
     279    {
     280        return "{$name}\s*+=\s*+(?!\"{$value}\"|'{$value}'|(?<=[=])(?!['\"]){$value}[\s/> ])";
     281    }
     282
     283    private function buildEqualsRegex(string $name, string $value): string
     284    {
     285        return "{$name}\s*+=\s*+(?:\"{$value}\"|'{$value}'|(?<=[=])(?!['\"]){$value}[\s/> ])";
     286    }
     287
     288    private function buildContainsWordRegex(string $name, string $value): string
     289    {
     290        return "{$name}\s*+=\s*+(?:\"[^\"]*?(?<=[\" ]){$value}[\" ]"
     291            . "|"
     292            . "'[^']*?(?<=[' ]){$value}[ ']"
     293            . "|"
     294            . "(?<==)(?!['\"]){$value}[ />])";
     295    }
     296
     297    private function buildContainsSubstringRegex(string $name, string $value): string
     298    {
     299        return "{$name}\s*+=\s*+(?:\"[^\"]*?{$value}|'[^']*?{$value}|(?<==)(?!['\"])[^\s]*?{$value})";
    272300    }
    273301
     
    299327            foreach ($names as $name) {
    300328                $matches[] = self::htmlNestedElementToken($name);
    301 
    302                 return implode('|', $matches);
    303             }
    304         }
    305 
    306         $namesRegex = '(?:' . implode('|', $names) . ')';
    307 
    308         return $elementObject->voidElementOrStartTagOnly ?
    309             '(?:' . self::htmlVoidElementToken($namesRegex) . '|' . self::htmlStartTagToken($namesRegex) . ')' :
    310             self::htmlElementsToken($names);
     329            }
     330
     331            return implode('|', $matches);
     332        }
     333
     334        return self::htmlElementMatchForBuilder(
     335            $names,
     336            $elementObject->voidElementOrStartTagOnly ?? false
     337        );
     338    }
     339
     340    // in Parser
     341    private static function htmlElementMatchForBuilder(array $names, bool $voidOrStartTagOnly): string
     342    {
     343        $nameRegex = '(?:' . implode('|', $names) . ')';
     344        $attrsToken = self::htmlAttributesListToken();
     345        $endTagRegex = self::htmlEndTagToken($nameRegex);
     346        $contentRegex = self::htmlTextContentToken($nameRegex);
     347
     348        if ($voidOrStartTagOnly) {
     349            // e.g. <link ...>, <meta ...>, <img ...>
     350            return "<(?<name>{$nameRegex})\b(?:\s++(?<attributes>{$attrsToken}+))?/?>";
     351        }
     352
     353        // Non-void element: capture inner content
     354        $startTag = "<(?<name>{$nameRegex})\b(?:\s++(?<attributes>{$attrsToken}+))?>";
     355        $content = "(?<content>{$contentRegex})";
     356        $endTag = "(?<endTag>{$endTagRegex})";
     357
     358        return $startTag . $content . $endTag;
    311359    }
    312360
  • jch-optimize/trunk/lib/src/Optimize.php

    r3310120 r3412704  
    3131use function ini_get;
    3232use function ini_set;
    33 use function preg_replace;
    3433use function version_compare;
    3534
     
    4544    use ContainerAwareTrait;
    4645
    47     private string $jit = '1';
     46    private string $jit;
    4847
    4948    /**
     
    5958        private UtilityInterface $utility
    6059    ) {
     60        $this->jit = ini_get('pcre.jit');
     61
     62        self::setPcreLimits();
     63
     64        if (version_compare(PHP_VERSION, '8.0', '<')) {
     65            throw new Exception\RuntimeException('PHP Version less than 8.0, Exiting plugin...');
     66        }
     67    }
     68
     69    public static function setPcreLimits(): void
     70    {
    6171        ini_set('pcre.backtrack_limit', '1000000');
    6272        ini_set('pcre.recursion_limit', '1000000');
    63 
    64         if (version_compare(PHP_VERSION, '7.0.0', '>=')) {
    65             $this->jit = ini_get('pcre.jit');
    66             ini_set('pcre.jit', '0');
    67         }
    68 
    69         if (version_compare(PHP_VERSION, '7.3', '<')) {
    70             throw new Exception\RuntimeException('PHP Version less than 7.3, Exiting plugin...');
    71         }
    72 
    73         $pcre_version = preg_replace('#(^\d++\.\d++).++$#', '$1', PCRE_VERSION);
    74 
    75         if (version_compare($pcre_version, '7.2', '<')) {
    76             throw new Exception\RuntimeException('PCRE Version less than 7.2. Exiting plugin...');
    77         }
     73        ini_set('pcre.jit', '0');
    7874    }
    7975
     
    107103        }
    108104
    109         if (version_compare(PHP_VERSION, '7.0.0', '>=')) {
    110             ini_set('pcre.jit', (string)$this->jit);
    111         }
     105        ini_set('pcre.jit', (string)$this->jit);
    112106
    113107        return $optimizedHtml;
     
    118112     * Inline CSS and JS will also be minified if respective parameters are set
    119113     *
    120      * @param string $html
     114     * @param   string $html
    121115     *
    122116     * @return string                       Optimized HTML
  • jch-optimize/trunk/lib/src/PageCache/PageCache.php

    r3402713 r3412704  
    399399
    400400        if ($this->input->server->get('REQUEST_METHOD') == 'POST') {
    401             if ($this->params->get('page_cache_exclude_form_users', '1')) {
     401            if ($this->params->get('page_cache_exclude_form_users', '1') && !empty($_POST)) {
    402402                $this->hooks->onUserPostForm();
    403403
  • jch-optimize/trunk/lib/src/Preloads/Http2Preload.php

    r3362084 r3412704  
    314314            case 'font':
    315315                if ($this->fontPreloads->count() < 2) {
    316                     $this->fontPreloads->attach($preload);
     316                    $this->fontPreloads->offsetSet($preload);
    317317                }
    318318                break;
    319319            case 'image':
    320320                if ($this->imagePreloads->count() < 4 || $preload->getFetchPriority() == 'high') {
    321                     $this->imagePreloads->attach($preload);
     321                    $this->imagePreloads->offsetSet($preload);
    322322                }
    323323                break;
    324324            case 'style':
    325                 $this->stylePreloads->attach($preload);
     325                $this->stylePreloads->offsetSet($preload);
    326326                break;
    327327            case 'script':
    328                 $this->scriptPreloads->attach($preload);
     328                $this->scriptPreloads->offsetSet($preload);
    329329                break;
    330330            default:
    331                 $this->otherPreloads->attach($preload);
     331                $this->otherPreloads->offsetSet($preload);
    332332                break;
    333333        }
  • jch-optimize/trunk/lib/src/Preloads/Preconnector.php

    r3362084 r3412704  
    7575        //If google fonts were optimized then add the fonts domain to preconnects if necessary
    7676        if ($this->googleFontsOptimized) {
    77             $this->preconnects->attach(
     77            $this->preconnects->offsetSet(
    7878                new Preconnect(Utils::uriFor('https://fonts.gstatic.com'), 'anonymous')
    7979            );
     
    137137                }
    138138
    139                 $this->preconnects->attach(new Preconnect($uri, $crossorigin));
     139                $this->preconnects->offsetSet(new Preconnect($uri, $crossorigin));
    140140            }
    141141        }
     
    148148
    149149            if (Uri::isAbsolute($uri) || Uri::isNetworkPathReference($uri)) {
    150                 $this->prefetches->attach(new DnsPrefetch($uri));
     150                $this->prefetches->offsetSet(new DnsPrefetch($uri));
    151151            }
    152152        }
     
    182182                    $linkObj = HtmlElementBuilder::load($preconnect);
    183183
    184                     $this->preconnects->attach(
     184                    $this->preconnects->offsetSet(
    185185                        new Preconnect(
    186186                            $linkObj->getHref(),
  • jch-optimize/trunk/lib/src/Preloads/PreloadsCollection.php

    r3362084 r3412704  
    3333     * @psalm-suppress ParamNameMismatch
    3434     */
    35     public function attach(object $object, mixed $info = null): void
     35    public function offsetSet(mixed $object, mixed $info = null): void
    3636    {
    3737        $this->rewind();
     
    5353                ) {
    5454                    if ($newExt == 'woff2') {
    55                         $this->detach($preload);
     55                        $this->offsetUnset($preload);
    5656                        break;
    5757                    }
     
    6565
    6666                    if ($newExt == 'woff' && $existingExt == 'ttf') {
    67                         $this->detach($preload);
     67                        $this->offsetUnset($preload);
    6868                        break;
    6969                    }
     
    7979        }
    8080
    81         parent::attach($object, $info);
     81        parent::offsetSet($object, $info);
     82    }
     83
     84    /**
     85     * @deprecated
     86     */
     87    public function attach(object $object, mixed $info = null): void
     88    {
     89        $this->offsetSet($object, $info);
     90    }
     91
     92    public function current(): Preload
     93    {
     94        return parent::current();
    8295    }
    8396}
  • jch-optimize/trunk/readme.txt

    r3402713 r3412704  
    33Contributors: codealfa
    44Tags: performance, pagespeed, cache, optimize, seo
    5 Tested up to: 6.8.3
    6 Stable tag: 5.1.0
     5Tested up to: 6.9
     6Stable tag: 5.1.1
    77License: GPLv3 or later
    8 Requires at least: 5.0
     8Requires at least: 6.5.0
    99Requires PHP: 8.0
    1010License URI: https://www.gnu.org/licenses/gpl-3.0.html
     
    8080
    8181== Changelog ==
     82
     83= 5.1.1 =
     84* Add icons to Admin menu
     85* Remove unused lazy load settings
     86* Update WordPress required version to 6.5.0
     87* Added additional LCP settings
     88* [HIGH] Fix error on Optimize Image tab
     89* [MEDIUM] Fix bug with handling invalid image URLs
     90* Improve Optimize CSS Delivery
     91* [MEDIUM] Files with integrity attribute were broken
     92* [LOW] Fix PHP 8.5 deprecations
     93
    8294= 5.1.0 =
    8395* Added support for AVIF images
  • jch-optimize/trunk/src/Html/Renderer/Setting.php

    r3402713 r3412704  
    177177        $options = [
    178178            '0'          => __('Disabled', 'jch-optimize'),
     179            '1'          => __('On save only', 'jch-optimize'),
    179180            'hourly'     => __('Hourly', 'jch-optimize'),
    180181            'twicedaily' => __('Twice Daily', 'jch-optimize'),
     
    182183        ];
    183184
    184         Helper::_('select.pro', __FUNCTION__, 0, $options);
     185        Helper::_('select.pro', __FUNCTION__, '1', $options);
    185186    }
    186187
     
    580581    {
    581582        Helper::_('switch', __FUNCTION__, '0');
    582     }
    583 
    584     public static function lazyload_autosize(): void
    585     {
    586         Helper::_('switch', __FUNCTION__, '1');
    587     }
    588 
    589     public static function pro_lazyload_effects(): void
    590     {
    591         Helper::_('switch.pro', __FUNCTION__, '0');
    592583    }
    593584
     
    699690
    700691    public static function pro_lcp_images(): void
     692    {
     693        Helper::_('multiselect.pro', __FUNCTION__, [], 'lazyload', 'file');
     694    }
     695
     696    public static function pro_lcp_identifiers(): void
     697    {
     698        Helper::_('multiselect.pro', __FUNCTION__, [], 'lazyload', 'file');
     699    }
     700
     701    public static function lcp_images_mobile(): void
     702    {
     703        Helper::_('multiselect.pro', __FUNCTION__, [], 'lazyload', 'file');
     704    }
     705
     706    public static function lcp_images_desktop(): void
    701707    {
    702708        Helper::_('multiselect.pro', __FUNCTION__, [], 'lazyload', 'file');
  • jch-optimize/trunk/src/Html/TabSettings.php

    r3402713 r3412704  
    678678                    __('Enable', 'jch-optimize'),
    679679                ],
    680                 'lazyload_autosize'          => [
    681                     __('Autosize images', 'jch-optimize'),
    682                     __(
    683                         'This setting attempts to maintain aspect ratio of images being lazy-loaded to prevent blank spaces under the images after they\'re loaded or distortions in rendering.'
    684                     )
    685                 ],
    686                 'pro_lazyload_effects'       => [
    687                     __('Enable effects', 'jch-optimize'),
    688                     __('Enable to use fade-in effects when images are scrolled into view.', 'jch-optimize')
    689                 ],
    690680                'pro_lazyload_iframe'        => [
    691681                    __('Lazy load iframes', 'jch-optimize'),
     
    804794                    __('LCP images', 'jch-optimize'),
    805795                    __(
    806                         'Add your LCP images here to have them preloaded with a high priority on whichever page they appear.'
    807                     )
     796                        'Add your LCP images here to have them preloaded with a high priority on whichever page they appear.',
     797                        'jch-optimize'
     798                    )
     799                ],
     800                'pro_lcp_identifiers' => [
     801                    __('LCP id/class', 'jch-optimize'),
     802                    __(
     803                        'Add the id or class of your LCP images here. Ensure the class is only shared by other LCP images, you only need one per page.',
     804                        'jch-optimize'
     805                    )
     806                ],
     807                'lcp_images_mobile' => [
     808                    __('Mobile only', 'jch-optimize'),
     809                    __('Add images here that will only be preloaded on mobile.', 'jch-optimize')
     810                ],
     811                'lcp_images_desktop' => [
     812                    __('Desktop only', 'jch-optimize'),
     813                    __('Add images here that will only be preloaded on desktop.', 'jch-optimize')
    808814                ]
    809815            ],
  • jch-optimize/trunk/src/Plugin/Installer.php

    r3402713 r3412704  
    103103/**
    104104 * Plugin Name: JCH Optimize Mode Switcher
    105  * Plugin URI: http://www.jch-optimize.net/
     105 * Plugin URI: https://www.jch-optimize.net/
    106106 * Description: Boost your WordPress site's performance with JCH Optimize as measured on PageSpeed
    107107 * Version: {VERSION}
     
    266266
    267267        if (
    268             file_exists($installedMU) && md5_file(
    269                                              JCH_PLUGIN_DIR . 'mu-plugins/jch-optimize-mode-switcher.php'
    270                                          ) !== md5_file($installedMU)
     268            file_exists($installedMU)
     269            && md5_file(
     270                JCH_PLUGIN_DIR . 'mu-plugins/jch-optimize-mode-switcher.php'
     271            ) !== md5_file($installedMU)
    271272        ) {
    272273            $this->installMUPlugin();
  • jch-optimize/trunk/src/Plugin/Loader.php

    r3402713 r3412704  
    164164
    165165            $reCacheModel = $this->getContainer()->get(ReCache::class);
    166             /** @see ReCache::reCache() */
    167             add_action(ReCache::RECACHE_CRON_HOOK, [$reCacheModel, 'reCache']);
    168             /** @see ReCache::reSchedule() */
    169             add_action('update_option_jch-optimize_settings', [$reCacheModel, 'reSchedule']);
     166            if ($this->params->get('recache_frequency') != '0') {
     167                /** @see ReCache::reCache() */
     168                add_action(ReCache::RECACHE_CRON_HOOK, [$reCacheModel, 'reCache']);
     169                /** @see ReCache::reSchedule() */
     170                add_action('update_option_jch-optimize_settings', [$reCacheModel, 'reSchedule']);
     171            }
    170172        }
    171173    }
  • jch-optimize/trunk/src/View/OptimizeImageHtml.php

    r3402713 r3412704  
    5555        wp_add_inline_script('jch-platform-wordpress', $js, 'before');
    5656
    57         wp_register_script('jch-uuid', JCH_PLUGIN_URL . 'media/uuid/uuidv4.js');
    58         wp_enqueue_script('jch-uuid');
    59 
    6057        if (JCH_PRO) {
    61             wp_register_script_module('jch-optimize-image', JCH_PLUGIN_URL . 'media/core/js/optimize-image.js', [
    62                 'jquery',
    63                 'jch-admin-utility',
    64                 'jch-platform-wordpress',
    65                 'jch-bootstrap',
    66                 'jch-uuid'
    67             ], JCH_VERSION, true);
    68 
     58            wp_register_script_module(
     59                'jch-optimize-image',
     60                JCH_PLUGIN_URL . 'media/core/js/optimize-image.js',
     61                [],
     62                JCH_VERSION
     63            );
    6964            wp_enqueue_script_module('jch-optimize-image');
    7065        }
    7166
    7267        wp_enqueue_style('jch-file-tree');
    73 
    7468        wp_enqueue_script('jch-file-tree');
    7569
    7670        $params = json_encode([
    77                 'auth' => [
    78                     'dlid' => $this->params->get('pro_downloadid', ''),
    79                     'secret' => '11e603aa',
    80                 ],
    81                 'resize_mode' => $this->params->get('pro_api_resize_mode', '1') ? 'auto' : 'manual',
    82                 'webp' => (bool)$this->params->get('pro_next_gen_images', '1'),
    83                 'avif' => (bool)$this->params->get('gen_avif_images', '1'),
    84                 'lossy' => (bool)$this->params->get('lossy', '1'),
    85                 'save_metadata' => (bool)$this->params->get('save_metadata', '0'),
    86                 'quality' => $this->params->get('quality', '85'),
    87                 'cropgravity' => $this->params->get('cropgravity', []),
    88                 'responsive' => (bool)$this->params->get('pro_gen_responsive_images', '1')
    89             ]);
     71            'auth'          => [
     72                'dlid' => $this->params->get('pro_downloadid', ''),
     73                'secret' => '11e603aa',
     74            ],
     75            'resize_mode'  => $this->params->get('pro_api_resize_mode', '1') ? 'auto' : 'manual',
     76            'webp'          => (bool)$this->params->get('pro_next_gen_images', '1'),
     77            'avif'          => (bool)$this->params->get('gen_avif_images', '1'),
     78            'lossy'        => (bool)$this->params->get('lossy', '1'),
     79            'save_metadata' => (bool)$this->params->get('save_metadata', '0'),
     80            'quality'      => $this->params->get('quality', '85'),
     81            'cropgravity'  => $this->params->get('cropgravity', []),
     82            'responsive'    => (bool)$this->params->get('pro_gen_responsive_images', '1')
     83        ]);
    9084
    9185
     
    9387        $noProID = __('Please enter your Download ID on the Configurations tab.', 'jch-optimize');
    9488
    95         $script =  <<<JS
     89        $script = <<<JS
    9690window.jchOptimizeImageData ={
    9791    message : '{$message}',
     
    10195JS;
    10296        wp_add_inline_script('jch-platform-wordpress', $script, 'before');
    103 
    10497    }
    10598}
  • jch-optimize/trunk/tmpl/pagecache_filters.php

    r3402713 r3412704  
    2626        </button>
    2727        <button id="recache-button" type="submit" name="action" value="recache" class="btn btn-outline-dark ms-1"
    28             <?php if (!JCH_PRO): ?>
     28            <?php if (!JCH_PRO) : ?>
    2929                disabled
    3030            <?php endif; ?>
    3131        >
    3232            <span class="fa fa-refresh"></span> Recache
    33             <?php if (!JCH_PRO): ?>
    34                 <span style="font-size: 0.6em; display: inline; padding-left: 5px;"><span class="fa fa-lock"></span> Pro</span>
     33            <?php if (!JCH_PRO) : ?>
     34                <span style="font-size: 0.6em; display: inline; padding-left: 5px;">
     35                    <span class="fa fa-lock"></span>
     36                    Pro
     37                </span>
    3538            <?php endif; ?>
    3639        </button>
     
    4851            <i>Storage: <span class="badge bg-primary"><?= $adapter ?></span> </i>
    4952            <i class="ms-2">Http Request:
    50                 <?php if ($httpRequest == 'yes'): ?>
     53                <?php if ($httpRequest == 'yes') : ?>
    5154                    <span class="badge bg-success">On</span>
    52                 <?php else: ?>
     55                <?php else : ?>
    5356                    <span class="badge bg-danger">Off</span>
    5457                <?php endif; ?>
     
    7780    <?= $filterAdapterSelectHtml ?>
    7881    <?php if (JCH_PRO) : ?>
    79     <?= $filterHttpRequestSelectHtml ?>
     82        <?= $filterHttpRequestSelectHtml ?>
    8083    <?php endif; ?>
    8184    <button id="clear-search" type="submit" class="btn btn-secondary ms-2">Clear Filters</button>
    8285    <script>
    8386        document.getElementById('clear-search').addEventListener('click', function (event) {
    84             document.getElementById('filter_search').value = '';
    85             document.getElementById('filter_time-1').value = '';
    86             document.getElementById('filter_time-2').value = '';
    87             document.getElementById('filter_device').value = '';
    88             document.getElementById('filter_adapter').value = '';
    89             document.getElementById('filter_http-request').value = '';
    90             document.getElementById('list_limit').value = '';
    91             document.getElementById('list_fullordering').value = '';
     87            document.getElementById('filter_search').value = ''
     88            document.getElementById('filter_time-1').value = ''
     89            document.getElementById('filter_time-2').value = ''
     90            document.getElementById('filter_device').value = ''
     91            document.getElementById('filter_adapter').value = ''
     92            document.getElementById('filter_http-request').value = ''
     93            document.getElementById('list_limit').value = ''
     94            document.getElementById('list_fullordering').value = ''
    9295        })
    9396    </script>
  • jch-optimize/trunk/vendor/composer/installed.php

    r3402713 r3412704  
    44        'pretty_version' => 'dev-master',
    55        'version' => 'dev-master',
    6         'reference' => '4541b13d2299de32ec35f80a1b6ae3a4f2ed960a',
     6        'reference' => '781c9a7e436505de7b07b7f0532d685389ad31db',
    77        'type' => 'library',
    88        'install_path' => __DIR__ . '/../../',
     
    1414            'pretty_version' => 'dev-master',
    1515            'version' => 'dev-master',
    16             'reference' => '4541b13d2299de32ec35f80a1b6ae3a4f2ed960a',
     16            'reference' => '781c9a7e436505de7b07b7f0532d685389ad31db',
    1717            'type' => 'library',
    1818            'install_path' => __DIR__ . '/../../',
  • jch-optimize/trunk/version.php

    r3402713 r3412704  
    1515defined('_JCH_EXEC') or die;
    1616
    17 const JCH_VERSION  = '5.1.0';
    18 const JCH_DATE     = '2025-11-25';
     17const JCH_VERSION  = '5.1.1';
     18const JCH_DATE     = '2025-12-06';
    1919const JCH_PRO      = '0';
    2020const JCH_DEVELOP  = '0';
Note: See TracChangeset for help on using the changeset viewer.